Skip to content

Commit 20b05fc

Browse files
authored
storybook: add markdown support (#91817)
Add mdx support for stories. This allows us to write mdx compliant markup which is commonly supported by documentation framework and friendlier to write than jsx. The mdx loader also conveniently exports a default export which means that we can omit the concept of storybook entirely
1 parent 1b4ce1b commit 20b05fc

10 files changed

+1324
-431
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@
182182
"@eslint/compat": "^1.2.7",
183183
"@eslint/eslintrc": "^3.3.0",
184184
"@eslint/js": "^9.22.0",
185+
"@mdx-js/loader": "^3.1.0",
185186
"@pmmmwh/react-refresh-webpack-plugin": "0.5.16",
186187
"@sentry/jest-environment": "6.0.0",
187188
"@sentry/profiling-node": "9.16.1",
@@ -255,6 +256,7 @@
255256
"dev-ui-admin": "SENTRY_ADMIN_UI_DEV=1 yarn dev-ui",
256257
"dev-ui-admin-rspack": "SENTRY_ADMIN_UI_DEV=1 yarn dev-ui-rspack",
257258
"dev-ui-storybook": "STORYBOOK_TYPES=1 yarn dev-ui",
259+
"dev-ui-storybook-rspack": "STORYBOOK_TYPES=1 yarn dev-ui-rspack",
258260
"dev-acceptance": "NO_DEV_SERVER=1 NODE_ENV=development webpack --watch",
259261
"dev-acceptance-rspack": "NO_DEV_SERVER=1 NODE_ENV=development rspack --watch",
260262
"webpack-profile": "NO_TS_FORK=1 webpack --profile --json > stats.json",

rspack.config.ts

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
DevServer,
88
OptimizationSplitChunksCacheGroup,
99
RspackPluginInstance,
10+
RuleSetRule,
1011
} from '@rspack/core';
1112
import rspack from '@rspack/core';
1213
import ReactRefreshRspackPlugin from '@rspack/plugin-react-refresh';
@@ -170,6 +171,35 @@ for (const locale of supportedLocales) {
170171
};
171172
}
172173

174+
const swcReactLoaderConfig: RuleSetRule['options'] = {
175+
jsc: {
176+
experimental: {
177+
plugins: [
178+
[
179+
'@swc/plugin-emotion',
180+
{
181+
sourceMap: true,
182+
// The "dev-only" option does not seem to apply correctly
183+
autoLabel: DEV_MODE ? 'always' : 'never',
184+
},
185+
],
186+
],
187+
},
188+
parser: {
189+
syntax: 'typescript',
190+
tsx: true,
191+
},
192+
transform: {
193+
react: {
194+
runtime: 'automatic',
195+
development: DEV_MODE,
196+
refresh: DEV_MODE,
197+
importSource: '@emotion/react',
198+
},
199+
},
200+
},
201+
};
202+
173203
/**
174204
* Main Webpack config for Sentry React SPA.
175205
*/
@@ -217,34 +247,19 @@ const appConfig: Configuration = {
217247
test: /\.(js|jsx|ts|tsx)$/,
218248
exclude: /\/node_modules\//,
219249
loader: 'builtin:swc-loader',
220-
options: {
221-
jsc: {
222-
experimental: {
223-
plugins: [
224-
[
225-
'@swc/plugin-emotion',
226-
{
227-
sourceMap: true,
228-
// The "dev-only" option does not seem to apply correctly
229-
autoLabel: DEV_MODE ? 'always' : 'never',
230-
},
231-
],
232-
],
233-
},
234-
parser: {
235-
syntax: 'typescript',
236-
tsx: true,
237-
},
238-
transform: {
239-
react: {
240-
runtime: 'automatic',
241-
development: DEV_MODE,
242-
refresh: DEV_MODE,
243-
importSource: '@emotion/react',
244-
},
245-
},
250+
options: swcReactLoaderConfig,
251+
},
252+
{
253+
test: /\.mdx?$/,
254+
use: [
255+
{
256+
loader: 'builtin:swc-loader',
257+
options: swcReactLoaderConfig,
246258
},
247-
},
259+
{
260+
loader: '@mdx-js/loader',
261+
},
262+
],
248263
},
249264
{
250265
test: /\.po$/,

static/app/styles/colors.mdx

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
import {useTheme} from '@emotion/react';
2+
import styled from '@emotion/styled';
3+
4+
import DoAccentColors from 'sentry-images/stories/color/do-accent-colors.svg';
5+
import DoContrast from 'sentry-images/stories/color/do-contrast.svg';
6+
import DoDifferentiation from 'sentry-images/stories/color/do-differentiation.svg';
7+
import DontAccentColors from 'sentry-images/stories/color/dont-accent-colors.svg';
8+
import DontContrast from 'sentry-images/stories/color/dont-contrast.svg';
9+
import DontDifferentiation from 'sentry-images/stories/color/dont-differentiation.svg';
10+
11+
import ExternalLink from 'sentry/components/links/externalLink';
12+
import Panel from 'sentry/components/panels/panel';
13+
import PanelItem from 'sentry/components/panels/panelItem';
14+
import ThemeToggle from 'sentry/components/stories/themeToggle';
15+
import {IconCheckmark, IconClose} from 'sentry/icons';
16+
import {space} from 'sentry/styles/space';
17+
18+
export const GRAY_PALETTES = [
19+
[{color: 'gray500', text: 'lightModeWhite'}],
20+
[{color: 'gray400', text: 'lightModeWhite'}],
21+
[{color: 'gray300', text: 'lightModeWhite'}],
22+
[{color: 'gray200', text: 'lightModeBlack'}],
23+
[{color: 'gray100', text: 'lightModeBlack'}],
24+
];
25+
26+
export const LEVELS_PALETTES = [
27+
[
28+
{color: 'purple400', text: 'lightModeWhite'},
29+
{color: 'purple300', text: 'lightModeWhite'},
30+
{color: 'purple200', text: 'lightModeBlack'},
31+
{color: 'purple100', text: 'lightModeBlack'},
32+
],
33+
[
34+
{color: 'blue400', text: 'lightModeWhite'},
35+
{color: 'blue300', text: 'lightModeWhite'},
36+
{color: 'blue200', text: 'lightModeBlack'},
37+
{color: 'blue100', text: 'lightModeBlack'},
38+
],
39+
[
40+
{color: 'green400', text: 'lightModeWhite'},
41+
{color: 'green300', text: 'lightModeBlack'},
42+
{color: 'green200', text: 'lightModeBlack'},
43+
{color: 'green100', text: 'lightModeBlack'},
44+
],
45+
[
46+
{color: 'yellow400', text: 'lightModeBlack'},
47+
{color: 'yellow300', text: 'lightModeBlack'},
48+
{color: 'yellow200', text: 'lightModeBlack'},
49+
{color: 'yellow100', text: 'lightModeBlack'},
50+
],
51+
[
52+
{color: 'red400', text: 'lightModeWhite'},
53+
{color: 'red300', text: 'lightModeWhite'},
54+
{color: 'red200', text: 'lightModeBlack'},
55+
{color: 'red100', text: 'lightModeBlack'},
56+
],
57+
[
58+
{color: 'pink400', text: 'lightModeWhite'},
59+
{color: 'pink300', text: 'lightModeWhite'},
60+
{color: 'pink200', text: 'lightModeBlack'},
61+
{color: 'pink100', text: 'lightModeBlack'},
62+
],
63+
];
64+
65+
export const FixedWidth = styled('div')`
66+
max-width: 800px;
67+
`;
68+
69+
export const SideBySideList = styled('ul')`
70+
list-style-type: none;
71+
margin: 0;
72+
padding: 0;
73+
& > li {
74+
margin: 0;
75+
}
76+
& > li > div {
77+
margin-bottom: 0;
78+
}
79+
80+
display: grid;
81+
grid-template-columns: 1fr 1fr;
82+
gap: ${space(2)};
83+
`;
84+
85+
export const PalettePanel = styled(Panel)`
86+
margin-bottom: 0;
87+
`;
88+
89+
export const PalettePanelItem = styled(PanelItem)`
90+
flex-direction: column;
91+
gap: ${space(0.5)};
92+
93+
&:first-child {
94+
border-radius: ${p => p.theme.borderRadius} ${p => p.theme.borderRadius} 0 0;
95+
}
96+
&:last-child {
97+
border-radius: 0 0 ${p => p.theme.borderRadius} ${p => p.theme.borderRadius};
98+
}
99+
&:first-child:last-child {
100+
border-radius: ${p => p.theme.borderRadius};
101+
}
102+
103+
background: ${p => p.theme[p.color]};
104+
color: ${p => p.theme[p.text]};
105+
`;
106+
107+
export const ExampleImg = styled('img')`
108+
border: 1px solid ${p => p.theme.border};
109+
border-radius: ${p => p.theme.borderRadius};
110+
max-width: 400px;
111+
`;
112+
113+
export const PositiveLabel = styled(({className}) => (
114+
<div className={className}>
115+
<IconCheckmark />
116+
DO
117+
</div>
118+
))`
119+
color: ${p => p.theme.green400};
120+
align-items: center;
121+
display: flex;
122+
font-weight: ${p => p.theme.fontWeightBold};
123+
gap: ${space(0.5)};
124+
`;
125+
126+
export const NegativeLabel = styled(({className}) => (
127+
<div className={className}>
128+
<IconClose color="red400" />
129+
DON'T
130+
</div>
131+
))`
132+
color: ${p => p.theme.red400};
133+
align-items: center;
134+
display: flex;
135+
font-weight: ${p => p.theme.fontWeightBold};
136+
gap: ${space(0.5)};
137+
`;
138+
139+
export const ExampleCardGrid = styled('figcaption')`
140+
display: grid;
141+
grid-template-columns: 1fr 2fr;
142+
align-items: flex-start;
143+
color: ${p => p.theme.subText};
144+
padding: ${space(1)} ${space(1)} 0;
145+
`;
146+
147+
export const ExampleCard = ({imgSrc, text, isPositive}) => (
148+
<figure>
149+
<ExampleImg src={imgSrc} />
150+
<ExampleCardGrid>
151+
{isPositive ? <PositiveLabel /> : <NegativeLabel />}
152+
<span>{text}</span>
153+
</ExampleCardGrid>
154+
</figure>
155+
);
156+
157+
export const ColorPalette = ({name, palette}) => {
158+
const theme = useTheme();
159+
160+
return (
161+
<SideBySideList>
162+
{palette.map((section, i) => {
163+
return (
164+
<li key={`${name}-${i}`}>
165+
<PalettePanel typeof="ul">
166+
{section.map((color, index) => {
167+
return (
168+
<PalettePanelItem
169+
key={`${name}-${color.color}-${index}`}
170+
color={color.color}
171+
text={color.text}
172+
>
173+
<strong>{color.color}</strong>
174+
{theme[color.color]}
175+
</PalettePanelItem>
176+
);
177+
})}
178+
</PalettePanel>
179+
</li>
180+
);
181+
})}
182+
</SideBySideList>
183+
);
184+
}
185+
186+
# Colors
187+
188+
Sentry has a flexible, tiered color system that adapts to both light and dark mode. Our color palette consists of neutral grays and 6 accent colors.
189+
190+
## Grays
191+
192+
There are 5 shades of gray, ranging from Gray 500 (darkest) to Gray 100 (lightest).
193+
194+
**Gray 300 and above** are accessible foreground colors that conform to [WCAG standards](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html). Use them as text and icon colors.
195+
196+
Here are the recommended use cases:
197+
198+
- **Gray 500:** headings, button labels, tags/badges, and alerts.
199+
- **Gray 400:** body text, input values & labels.
200+
- **Gray 300:** input placeholders, inactive/disabled inputs and buttons, chart labels, supplemental and non-essential text
201+
- **Gray 200:** borders around large elements (cards, panels, dialogs, tables).
202+
- **Gray 100:** dividers and borders around small elements (buttons, form inputs). */}
203+
204+
<ThemeToggle>
205+
<ColorPalette name="grays" palette={GRAY_PALETTES} />
206+
</ThemeToggle>
207+
208+
## Accent Colors
209+
210+
Accent colors help shift the user's focus to certain interactive and high-priority elements, like links, buttons, and warning banners.
211+
212+
### Hues
213+
214+
There are 6 hues to choose from. Each has specific connotations:
215+
216+
- **Purple:** brand, current/active/focus state, or new information.
217+
- **Blue:** hyperlink.
218+
- **Green:** success, resolution, approval, availability, or creation.
219+
- **Yellow:** warning, missing, or impeded progress.
220+
- **Red:** fatal error, deletion, or removal.
221+
- **Pink:** new feature or promotion.
222+
223+
### Levels
224+
225+
Each hue comes in 4 levels: 400 (dark), 300 (full opacity), 200 (medium opacity), and 100 (low opacity).
226+
227+
- **The 400 level** is a darkened version of 300. It is useful for hover/active states in already accentuated elements. For example, a button could have a background of Purple 300 in normal state and Purple 400 on hover.
228+
- **The 300 level** has full opacity and serves well as text and icon colors (with the exception of Yellow 300, which does not meet [WCAG's contrast standards](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html)).
229+
- **The 200 level** has medium opacity, useful for borders and dividers.
230+
- **The 100 level** has very low opacity, useful as background fills.
231+
232+
<ThemeToggle>
233+
<ColorPalette name="levels" palette={LEVELS_PALETTES} />
234+
</ThemeToggle>
235+
236+
## Accessibility
237+
238+
When it comes to using color, there are two main accessibility concerns: readability and separation.
239+
240+
### Readability
241+
242+
[WCAG](https://www.w3.org/TR/WCAG21/) requires that normal text elements have a contrast ratio of at least 4.5:1 against the background. For large text (at least 16px in size AND in medium/bold weight), the required ratio is lower, at 3:1. This is to ensure a comfortable reading experience in different lighting conditions. [Use this tool](https://webaim.org/resources/contrastchecker/) to confirm text contrast ratios.
243+
244+
In Sentry's color palette, only Gray 300 and above satisfy the contrast requirement for normal text. This applies to both light and dark mode.
245+
246+
Accent colors in the 300 series, except for Yellow 300, satisfy the contrast requirement for large text.
247+
248+
<SideBySideList>
249+
<ExampleCard
250+
imgSrc={DoContrast}
251+
text="Use Gray 300 and above for normal text"
252+
isPositive
253+
/>
254+
<ExampleCard
255+
imgSrc={DontContrast}
256+
text="Use Gray 100 or 200 for normal text, as they don't have the required the contrast levels"
257+
/>
258+
<ExampleCard
259+
imgSrc={DoAccentColors}
260+
text="Use accent colors in the 300 series (except for Yellow 300) for large text, if needed"
261+
isPositive
262+
/>
263+
<ExampleCard
264+
imgSrc={DontAccentColors}
265+
text="Use accent colors in the 100 or 200 series for any text"
266+
/>
267+
</SideBySideList>
268+
269+
### Separation
270+
271+
Color can be an effective way to visually separate elements in the user interface. However, not all users see color in the same way. Some are color-blind and cannot reliably differentiate one color from another. Some have color filters on their screens, like Night Shift in MacOS. Others are in bright environments with high levels of glare, reducing their ability to see color clearly.
272+
273+
As such, color is an unreliable way to separate elements. Whenever possible, provide additional visual cues like icons, text labels, line type (solid, dashed, dotted),… to further reinforce the separation.
274+
275+
<SideBySideList>
276+
<ExampleCard
277+
imgSrc={DoDifferentiation}
278+
text="Provide additional visual encoding (e.g. line type) besides color to differentiate elements"
279+
isPositive
280+
/>
281+
<ExampleCard
282+
imgSrc={DontDifferentiation}
283+
text="Use color as the only way to differentiate elements"
284+
/>
285+
</SideBySideList>

0 commit comments

Comments
 (0)