Skip to content

Commit 94c5643

Browse files
natemoo-reJonasBagetsantry[bot]
authored andcommitted
feat(stories): add content & voice principles (#100845)
Bringing the **Content & Voice** doc in from Notion. Probably needs some small design updates, but the content is ready for review. --------- Co-authored-by: Jonas <[email protected]> Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent 6c64b12 commit 94c5643

File tree

8 files changed

+279
-1
lines changed

8 files changed

+279
-1
lines changed

knip.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const productionEntryPoints = [
1717
// this is imported with require.context
1818
'static/app/data/forms/*.tsx',
1919
// --- we should be able to get rid of those: ---
20+
// Only used in stories (so far)
21+
'static/app/components/core/quote/*.tsx',
2022
// Prevent exception until we build out coverage
2123
'static/app/components/prevent/virtualRenderers/**/*.{js,ts,tsx}',
2224
// todo we currently keep all icons

static/app/components/core/layout/container.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ interface ContainerLayoutProps {
105105
export type ContainerElement =
106106
| 'article'
107107
| 'aside'
108+
| 'blockquote'
108109
| 'div'
109110
| 'figure'
110111
| 'footer'

static/app/components/core/layout/stack.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ type StackLayoutProps = Pick<
1212
'align' | 'direction' | 'gap' | 'justify' | 'wrap'
1313
>;
1414

15-
type StackProps<T extends ContainerElement = 'div'> = StackLayoutProps & FlexProps<T>;
15+
export type StackProps<T extends ContainerElement = 'div'> = StackLayoutProps &
16+
FlexProps<T>;
1617

1718
const StackComponent = styled(
1819
<T extends ContainerElement = 'div'>({
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
---
2+
title: Content & Voice
3+
layout: document
4+
description: Sentry's unique brand voice sets us apart—learn how to approach content and voice in Sentry's UI.
5+
---
6+
7+
## Tone
8+
9+
There are two different ways to write copy within the Sentry app. There’s normal **plain speech**, and there’s our branded, personality-rich “**Sentry Voice**.” Sentry Voice is a little snarky (but never mean), jokey, and quick-witted. The jokes should be simple, if it needs explanation, it isn’t gonna work.
10+
11+
Generally, you should default to plain and clear speech in the product. This includes page names, table headings, widgets, CTAs, Seer responses, and descriptive text. Occasionally it will be appropriate to use the “Sentry Voice,” like on 404 pages, new product announcements and other areas that don’t directly interfere with someone’s workflow. Within the product, assume users are frustrated and in a hurry — above all our goal is to make sure they can find what they are looking for quickly, and explain what is happening clearly.
12+
13+
### Examples
14+
15+
**Plain speech**
16+
17+
> Issues are groups of errors that have a similar stacktrace. Set an alert for new issues, when an issue changes state, frequency of errors, or users affected by an issue.
18+
19+
**Sentry Voice**
20+
21+
> 404, Page not found. We looked. And then we gave up. [Button: *Return to a Real Page*]
22+
23+
> Sentry Rollback is here. Forgot everything you did this year? Don’t worry—we have the receipts. Take a look back at your 2024 with Sentry.
24+
25+
## When to Use “Sentry Voice” vs. Plain Speech
26+
27+
**Do** use Sentry Voice for…
28+
29+
- Designing a new product tour
30+
- Descriptions in the “What’s New” feature
31+
- Adding personality to a long (30+ second) loading state
32+
- High level loading states (404, 500, This Page Does Not Exist)
33+
- Onboarding, but use it sparingly. Make sure it is not getting in the way of helping users actually configure their account.
34+
- Empty states, also case-dependent. These should always have an actionable first line, but can have a snarky heading or second sentence.
35+
36+
**Don’t** use Sentry Voice (and use plain speech instead) for…
37+
38+
- Product UI. This includes page titles, labels, filters, search, table headings, CTAs, toasts, alerts, and callouts.
39+
- Non-marketing emails. Emails generated by weekly summaries or alerts should be straightforward and direct.
40+
- Settings pages. These are already complicated enough.
41+
- Documentation. Users are here to find information quickly and they need to be easily searchable.
42+
43+
## Writing in “Sentry Voice”
44+
45+
You should not be using this voice within regular workflows, or when someone is trying to complete a task. Even when using Sentry Voice make sure to front-load main information, and not let personality get in the way of content. For example in an error message, make sure the first few words of the body capture the problem first (_404 Page not found_. We looked. And then we gave up.).
46+
47+
Here are three things to keep in mind when writing with personality on behalf of Sentry:
48+
49+
1. **We have empathy for our users**: they should feel understood and like we appropriately grasp the gravity of their situation. Whatever they're going through, we get it. When we are snarky we are snarky towards the situation they are in, not towards the user themself.
50+
2. **We're self-aware**: look, we know were not curing cancer over here. We help developers, but we're not changing the world. Don't make bold claims with aspirational copy.
51+
3. **We have fun**: this stuff is dry, so look for opportunity to have fun with the copy (while keeping everything above in mind).
52+
4. **If you feel confused or unsure where and how to use Sentry Voice, ask our in-house copy writer in the `#discuss-design` Slack channel.**
53+
54+
> [!TIP] Heads up!
55+
> If you're looking for more information, this was modified from the [brand identity writing style guide](https://brand.getsentry.com/document/5#/style-guides/writing-style-guide).
56+
57+
## General Copywriting Guidelines
58+
59+
**Keep it simple**&mdash;avoid jargon and don’t get fancy. If you are referencing new words or concepts explain them or link out to documentation.
60+
61+
**Be informative**. Always provide straightforward, accurate information, we don't manipulate people into believing we’re anything more than we actually are.
62+
63+
**Use American spelling**. Apologies to our colleagues who prefer the Queen’s English.
64+
65+
**Avoid emotional punctuation**; for example use `!` sparingly.
66+
67+
**Don’t use weird punctuation** like the interrobang ``. Not everyone shares your affinity for esoteric glyphs.
68+
69+
**Be consistent.** Reference our [Icons + CTA Copy Table](https://sentry.sentry.io/stories/core/button#icons) to make sure you are using agreed upon terms in CTA Buttons.
70+
For example, Dashboards, Views, Monitors, and Automations are always “Edited” and use the pencil icon.
71+
When a user is creating a new Dashboard, View, or Monitor, the language should always be “Create [Object]” with a plus icon.
72+
73+
**Use punctuation thoughtfully**. If you don’t know how to use colons `:` or semicolons `;` it is better not to try. Alternatively, ask a copywriter for help.
74+
75+
> [!TIP] A brief aside on dashes
76+
> Hyphens, the `-` or `&hyphen;` symbol, are used to join words together or write compound numbers (_sixty-five_, _self-serve_).
77+
>
78+
> En-dashes, the `` or `&ndash;` symbol, are most commonly used for time ranges (_2020–2022_, _September 1–September 3)_.
79+
> Insert these without a space on either side.
80+
>
81+
> Em-dashes, the `` or `&mdash;` symbol, are the longest of the three and act more like true punctuation.
82+
> You can use these in place of a comma or parentheses, or to indicate when a sentence is taking a new turn.
83+
> (_We want to introduce users to Seer&mdash;a new name for our old AI product&mdash;which we have rebranded this month_.)
84+
>
85+
> For additional information, refer to [Merriam Webster](https://www.merriam-webster.com/grammar/em-dash-en-dash-how-to-use).
86+
87+
## Use Title Case for Titles and Headings
88+
89+
Within the app we use title case for all headings, CTAs and labels (including: chart titles, table, chart axes, chart legends, table titles, table column headers, form fields labels, anything in the sidebar).
90+
This means the first letter of every word is capitalized, except for conjunctions and “minor” words.
91+
92+
This is true for `<Heading>`, as well as all CTAs, table headers, and default widgets. As a rule, we **never** use all caps in the product. If you see `text-transform: uppercase` anywhere, you should remove it.
93+
94+
**Examples**
95+
96+
> Last Seen (column header)
97+
98+
> Save As… (CTA Button)
99+
100+
> Errors and Outages (sidebar item & H1)
101+
102+
> Most Frozen Frames (default widget title)
103+
104+
Not sure how to convert something? Refer to [capitalizemytitle.com](https://capitalizemytitle.com/) for a quick gut check.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {Quote} from './quote';
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
title: Quote
3+
description: A blockquote component for displaying extended quotations with optional source attribution and citations.
4+
source: 'sentry/components/core/quote'
5+
resources:
6+
js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/quote/quote.tsx
7+
---
8+
9+
import * as Storybook from 'sentry/stories';
10+
11+
import {Quote} from './quote';
12+
13+
import documentation from '!!type-loader!@sentry/scraps/quote';
14+
15+
export {documentation};
16+
17+
The `Quote` component renders a `<blockquote>` element to indicate that the enclosed text is an extended quotation. It provides semantic HTML with proper styling and optional citation support through the `source` prop.
18+
19+
## Basic Usage
20+
21+
A simple quotation without attribution:
22+
23+
<Storybook.Demo>
24+
<Quote>It’s not a bug; it’s an undocumented feature.</Quote>
25+
</Storybook.Demo>
26+
```jsx
27+
<Quote>It’s not a bug; it’s an undocumented feature.</Quote>
28+
```
29+
30+
## Props
31+
32+
### Source Attribution
33+
34+
The `source` prop accepts an object with optional `author`, `href`, and `label` properties for attribution and citation. These properties are rendered with proper semantic HTML using the `cite` attribute and `<cite>` element.
35+
36+
#### With Author
37+
38+
Provide attribution to a specific person or entity:
39+
40+
<Storybook.Demo>
41+
<Quote source={{author: 'Developer'}}>It’s not a bug, it’s a feature.</Quote>
42+
</Storybook.Demo>
43+
```jsx
44+
<Quote source={{author: 'Developer'}}>It’s not a bug, it’s a feature.</Quote>
45+
```
46+
47+
#### With Author and Label
48+
49+
Include both the author and additional context about the source:
50+
51+
<Storybook.Demo>
52+
<Quote source={{author: 'Developer', label: 'in complete denial'}}>
53+
It’s not a bug, it’s a feature.
54+
</Quote>
55+
</Storybook.Demo>
56+
```jsx
57+
<Quote source={{author: 'Developer', label: 'in complete denial'}}>
58+
It’s not a bug, it’s a feature.
59+
</Quote>
60+
```
61+
62+
#### With URL Reference
63+
64+
The `href` property sets the `cite` attribute on the `<blockquote>` element, providing a machine-readable source reference:
65+
66+
<Storybook.Demo>
67+
<Quote
68+
source={{
69+
author: 'Developer',
70+
label: 'in complete denial',
71+
href: 'https://example.com/source',
72+
}}
73+
>
74+
It’s not a bug, it’s a feature.
75+
</Quote>
76+
</Storybook.Demo>
77+
```jsx
78+
<Quote
79+
source={{
80+
author: 'Developer',
81+
label: 'in complete denial',
82+
href: 'https://example.com/source',
83+
}}
84+
>
85+
It’s not a bug, it’s a feature.
86+
</Quote>
87+
```
88+
89+
## Accessibility
90+
91+
`<Quote>` uses semantic HTML to ensure quotes are correctly announced by assistive technologies, including the `cite` element for machine-readable source references.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {Fragment, type ReactNode} from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import {Stack} from '@sentry/scraps/layout';
5+
import type {StackProps} from '@sentry/scraps/layout/stack';
6+
import {Text} from '@sentry/scraps/text';
7+
8+
// interface + type union because `extends` doesn't play nicely with generics
9+
interface QuoteBaseProps {
10+
children: ReactNode;
11+
source?: {
12+
author?: string;
13+
href?: string;
14+
label?: string;
15+
};
16+
}
17+
export type QuoteProps = QuoteBaseProps & Omit<StackProps<'blockquote'>, 'children'>;
18+
19+
export function Quote(props: QuoteProps) {
20+
const {children, ...spreadProps} = props;
21+
return (
22+
<Stack gap="md" as="figure" position="relative" {...spreadProps}>
23+
<Line aria-orientation="vertical" />
24+
<Blockquote cite={props.source?.href} as="blockquote">
25+
{children}
26+
</Blockquote>
27+
{props.source ? (
28+
<Caption>
29+
<Text as="p">
30+
&ndash;&nbsp;
31+
{props.source.author}
32+
{props.source?.label ? (
33+
<Fragment>
34+
, <cite>{props.source.label}</cite>
35+
</Fragment>
36+
) : null}
37+
</Text>
38+
</Caption>
39+
) : null}
40+
</Stack>
41+
);
42+
}
43+
44+
const Line = styled('hr')`
45+
position: absolute;
46+
top: 0;
47+
bottom: 0;
48+
left: 0;
49+
margin: 0;
50+
border: none;
51+
height: 100%;
52+
width: 1px;
53+
padding-left: ${p => p.theme.space.xl};
54+
margin-left: ${p => p.theme.space.lg};
55+
border-left: 1px solid ${p => p.theme.tokens.border.primary};
56+
`;
57+
58+
const Blockquote = styled('blockquote')`
59+
/**
60+
* Reset any properties that might be set by the global CSS styles.
61+
*/
62+
margin: 0;
63+
padding: 0;
64+
border: none;
65+
padding-left: calc(${p => `${p.theme.space.xl} + ${p.theme.space.lg}`});
66+
`;
67+
68+
const Caption = styled('figcaption')`
69+
/**
70+
* Reset any properties that might be set by the global CSS styles.
71+
*/
72+
margin: 0;
73+
padding: 0;
74+
border: none;
75+
padding-left: calc(${p => `${p.theme.space.xl} + ${p.theme.space.lg}`});
76+
`;

static/app/stories/view/storyMdxComponent.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from 'react';
33
import {type Callout as CalloutProps} from '@r4ai/remark-callout';
44

55
import {Alert, type AlertProps} from '@sentry/scraps/alert';
6+
import {Quote, type QuoteProps} from '@sentry/scraps/quote/quote';
67

78
import {InlineCode} from 'sentry/components/core/code';
89
import {Stack} from 'sentry/components/core/layout';
@@ -61,4 +62,5 @@ export const storyMdxComponents = {
6162
ul: (props: Omit<HTMLProps<HTMLUListElement>, 'wrap'>) => (
6263
<Stack {...props} as="ul" gap="lg" />
6364
),
65+
blockquote: (props: QuoteProps) => <Quote {...props} />,
6466
};

0 commit comments

Comments
 (0)