@canonical/code-standards 0.1.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.
@@ -0,0 +1,413 @@
1
+ # Storybook Standards
2
+
3
+ Standards for storybook development.
4
+
5
+ ## storybook/documentation/source
6
+
7
+ Component documentation must primarily come from TSDoc comments on the component signature and props. Storybook docs overrides should only be used when the documentation needs to differ between code and Storybook contexts.
8
+
9
+ ### Do
10
+
11
+ (Do) Use TSDoc comments for primary documentation.
12
+ ```typescript
13
+
14
+ // ContextualMenu/types.ts
15
+ export interface ContextualMenuProps extends HTMLAttributes<HTMLDivElement> {
16
+ /** The element that triggers the contextual menu */
17
+ children: ReactElement;
18
+ /** The items to display in the contextual menu */
19
+ items: MenuItem[];
20
+ }
21
+
22
+ // ContextualMenu/ContextualMenu.tsx
23
+ /**
24
+ * A wrapper component that adds a contextual menu to its children.
25
+ * The menu appears when the children are clicked or activated via keyboard.
26
+ */
27
+ const ContextualMenu = ({
28
+ children,
29
+ items,
30
+ }: ContextualMenuProps): ReactElement => {
31
+ // ...
32
+ };
33
+ ```
34
+
35
+ ### Don't
36
+
37
+ (Don't) Add documentation in Storybook parameters when TSDoc is sufficient.
38
+ ```typescript
39
+ export const Default: Story = {
40
+ parameters: {
41
+ docs: {
42
+ description: {
43
+ component: "A wrapper component that adds a contextual menu to its children" // Covered in TSDoc
44
+ }
45
+ }
46
+ }
47
+ };
48
+ ```
49
+
50
+ ---
51
+
52
+ ## storybook/story/decorator
53
+
54
+ Decorators must be used to wrap stories in common context providers or layout elements. Decorators should generally be defined in `storybook/decorators.tsx` and be imported as `decorators` in the storybook file. Decorators may be defined inline in story files in limited circumstances when it would be difficult to globally define a semantic decorator (e.g., a decorator that provides mock data specific to that component).
55
+
56
+ ### Do
57
+
58
+ (Do) Use decorators for static context or layout wrapping.
59
+ ```typescript
60
+
61
+ // storybook/decorators.tsx
62
+
63
+ export const theme = (Story: StoryFn<typeof Component>) => (
64
+ <ThemeProvider theme="dark">
65
+ <Story />
66
+ </ThemeProvider>
67
+ );
68
+
69
+ export const config = (Story: StoryFn<typeof Component>) => (
70
+ <ConfigProvider locale="en">
71
+ <Story />
72
+ </ConfigProvider>
73
+ );
74
+
75
+ // src/ui/<ComponentName>/<ComponentName>.stories.tsx
76
+
77
+ import * as decorators from "storybook/decorators.js";
78
+ export const WithTheme: Story = {
79
+ decorators: [decorators.theme],
80
+ args: {
81
+ variant: "primary"
82
+ }
83
+ };
84
+
85
+ // Component-level decorator in meta, applies to all stories in this file.
86
+ const meta = {
87
+ decorators: [decorators.config],
88
+ } satisfies Meta<typeof Component>;
89
+
90
+ export default meta;
91
+ ```
92
+
93
+ ### Don't
94
+
95
+ (Don't) Use function-based stories for static wrapping that could be done with decorators.
96
+ ```typescript
97
+ // Bad: Using function-based story for static wrapping
98
+ export const WithTheme = (args: ComponentProps) => (
99
+ <ThemeProvider theme="dark">
100
+ <Component {...args} />
101
+ </ThemeProvider>
102
+ );
103
+ ```
104
+
105
+ ---
106
+
107
+ ## storybook/story/documentation
108
+
109
+ Story documentation must focus on usage patterns and variations, not implementation details. Each story should demonstrate a specific use case or variation.
110
+
111
+ ### Do
112
+
113
+ (Do) Document usage patterns and variations.
114
+ ```typescript
115
+ export const WithCustomTrigger: Story = {
116
+ args: {
117
+ trigger: <button>Custom Trigger</button>
118
+ },
119
+ parameters: {
120
+ docs: {
121
+ description: {
122
+ story: "The component accepts a custom trigger element to replace the default button."
123
+ }
124
+ }
125
+ }
126
+ };
127
+ ```
128
+
129
+ ### Don't
130
+
131
+ (Don't) Document implementation details or internal behavior.
132
+ ```typescript
133
+ export const Default: Story = {
134
+ parameters: {
135
+ docs: {
136
+ description: {
137
+ story: "Uses React.createPortal internally to render the popup" // Bad: Implementation detail
138
+ }
139
+ }
140
+ }
141
+ };
142
+ ```
143
+
144
+ ---
145
+
146
+ ## storybook/story/format
147
+
148
+ Stories must use one of three formats, each serving specific needs:
149
+ 1. CSF3 (object-based) format must be used for standard component variations where args define the component state.
150
+ 2. Function-based format must be used when the story needs to directly control component rendering or wrap the component with custom elements.
151
+ 3. Template-based format must be used when multiple stories share the same logic but differ in their args, and the template differs from the component markup.
152
+
153
+ ### Do
154
+
155
+ (Do) Use the format that matches your specific use case.
156
+ ```typescript
157
+ // CSF3: For standard component variations through args
158
+ type Story = StoryObj<typeof meta>;
159
+
160
+ export const Default: Story = {
161
+ args: {
162
+ variant: "primary",
163
+ children: "Click me",
164
+ disabled: false
165
+ }
166
+ };
167
+
168
+ // Function-based: When story needs dynamic rendering logic
169
+ export const WithDynamicChildren = (args: ComponentProps) => {
170
+ const [items] = useState(["Item 1", "Item 2"]);
171
+ return (
172
+ <Component {...args}>
173
+ {items.map((item) => (
174
+ <ListItem key={item}>{item}</ListItem>
175
+ ))}
176
+ </Component>
177
+ );
178
+ };
179
+
180
+ // Template-based: When multiple stories share logic
181
+ const Template: StoryFn<typeof Component> = (args) => (
182
+ <div className="button-container">
183
+ <Label>Button:</Label>
184
+ <Component {...args} />
185
+ </div>
186
+ );
187
+
188
+ export const Primary = Template.bind({});
189
+ Primary.args = { variant: "primary" };
190
+ ```
191
+
192
+ ### Don't
193
+
194
+ (Don't) Use a format that doesn't match your use case.
195
+ ```typescript
196
+ // Bad: Using function format for simple args variation
197
+ export const SimpleButton = () => (
198
+ <Component variant="primary" disabled={false}>
199
+ Click me
200
+ </Component>
201
+ );
202
+
203
+ // Bad: Duplicating complex logic without template
204
+ export const First: Story = {
205
+ decorators: [(Story) => (
206
+ <div className="button-container">
207
+ <Label>Button:</Label>
208
+ <Story />
209
+ </div>
210
+ )]
211
+ };
212
+ ```
213
+
214
+ ---
215
+
216
+ ## storybook/story/import
217
+
218
+ Stories must import their component as 'Component' to maintain a consistent, generic reference that is decoupled from the specific component name.
219
+
220
+ ### Do
221
+
222
+ (Do) Import the component generically as 'Component'.
223
+ ```typescript
224
+ import Component from "./SkipLink.js";
225
+
226
+ const meta = {
227
+ title: "SkipLink",
228
+ component: Component,
229
+ } satisfies Meta<typeof Component>;
230
+
231
+ export const Default: Story = {
232
+ args: {
233
+ children: "Skip to main content"
234
+ }
235
+ };
236
+ ```
237
+
238
+ ### Don't
239
+
240
+ (Don't) Import the component using its specific name.
241
+ ```typescript
242
+ import SkipLink from "./SkipLink.js";
243
+
244
+ const meta = {
245
+ title: "SkipLink",
246
+ component: SkipLink, // Bad: Using specific component name
247
+ } satisfies Meta<typeof SkipLink>;
248
+ ```
249
+
250
+ ---
251
+
252
+ ## storybook/story/naming
253
+
254
+ Story names must be descriptive and follow a consistent pattern. Use PascalCase for story exports and natural language for story titles.
255
+
256
+ ### Do
257
+
258
+ (Do) Use clear, descriptive names that indicate the variation.
259
+ ```typescript
260
+ export const WithCustomStyles: Story = {
261
+ args: {
262
+ className: "custom",
263
+ children: "Styled Content"
264
+ },
265
+ parameters: {
266
+ docs: {
267
+ description: {
268
+ story: "Demonstrates custom styling options"
269
+ }
270
+ }
271
+ }
272
+ };
273
+ ```
274
+
275
+ ### Don't
276
+
277
+ (Don't) Use technical or implementation-focused names.
278
+ ```typescript
279
+ export const TestCase1: Story = { // Bad: Non-descriptive name
280
+ args: {
281
+ _testFlag: true, // Bad: Implementation detail
282
+ children: "Content"
283
+ }
284
+ };
285
+ ```
286
+
287
+ ---
288
+
289
+ ## storybook/story/organization
290
+
291
+ Stories must be organized into logical groups that demonstrate related features and variations. Each story should focus on a specific use case or feature, with clear naming that indicates what aspect of the component it demonstrates.
292
+
293
+ ### Do
294
+
295
+ (Do) Group related features with clear, descriptive names.
296
+ ```typescript
297
+ // Basic usage
298
+ export const Default: Story = {
299
+ args: {
300
+ mainId: "main",
301
+ children: "Skip to main content"
302
+ }
303
+ };
304
+
305
+ // Custom element targeting
306
+ export const CustomMainElement: Story = {
307
+ args: {
308
+ mainId: "my-main-element",
309
+ children: "Skip to main content"
310
+ }
311
+ };
312
+
313
+ // Content customization
314
+ export const CustomText: Story = {
315
+ args: {
316
+ mainId: "main",
317
+ children: "Jump to content"
318
+ }
319
+ };
320
+ ```
321
+
322
+ ### Don't
323
+
324
+ (Don't) Use unclear names or mix unrelated features in a single story.
325
+ ```typescript
326
+ // Bad: Unclear what this story demonstrates
327
+ export const Variant1: Story = {
328
+ args: {
329
+ mainId: "custom",
330
+ children: "Skip",
331
+ className: "special",
332
+ onClick: () => {},
333
+ style: { color: "red" }
334
+ }
335
+ };
336
+ ```
337
+
338
+ ---
339
+
340
+ ## storybook/story/testing
341
+
342
+ Stories that test component behavior must use the `play` function to simulate user interactions and verify expected outcomes.
343
+
344
+ ### Do
345
+
346
+ (Do) Use play functions to test interactive behavior.
347
+ ```typescript
348
+ export const InteractionTest: Story = {
349
+ args: {
350
+ children: "Click Me",
351
+ onClick: () => {}
352
+ },
353
+ play: async ({ canvasElement, args }) => {
354
+ const button = canvasElement.querySelector("button");
355
+ await userEvent.click(button);
356
+ await expect(args.onClick).toHaveBeenCalled();
357
+ }
358
+ };
359
+ ```
360
+
361
+ ### Don't
362
+
363
+ (Don't) Test implementation details or internal state.
364
+ ```typescript
365
+ export const InternalTest: Story = {
366
+ play: async ({ component }) => {
367
+ // Bad: Testing internal implementation
368
+ expect(component._internalState).toBe(true);
369
+ }
370
+ };
371
+ ```
372
+
373
+ ---
374
+
375
+ ## storybook/story/visibility
376
+
377
+ Stories that exist solely for testing or visual coverage but don't represent valid usage patterns must be hidden from documentation and the sidebar using tags.
378
+
379
+ ### Do
380
+
381
+ (Do) Hide test-only stories that don't represent valid usage.
382
+ ```typescript
383
+ export const FocusedState: Story = {
384
+ args: {
385
+ isOpen: true,
386
+ children: "Test Content"
387
+ },
388
+ tags: ["!dev", "!autodocs"],
389
+ play: async ({ canvasElement }) => {
390
+ const element = canvasElement.querySelector(".component");
391
+ element.focus();
392
+ }
393
+ };
394
+ ```
395
+
396
+ ### Don't
397
+
398
+ (Don't) Show implementation details or test states in documentation.
399
+ ```typescript
400
+ export const InternalTestState: Story = {
401
+ args: {
402
+ _internalProp: true, // Bad: Exposing internal state
403
+ children: "Test"
404
+ },
405
+ parameters: {
406
+ docs: {
407
+ description: { story: "Tests internal state" } // Bad: Implementation detail
408
+ }
409
+ }
410
+ };
411
+ ```
412
+
413
+ ---
@@ -0,0 +1,163 @@
1
+ # Styling Standards
2
+
3
+ Standards for styling development.
4
+
5
+ ## styling/themes/definition
6
+
7
+ Themes are collections of semantic tokens that provide consistent styling across components. See css/themes/activation for implementation details.
8
+
9
+ ### Do
10
+
11
+ (Do) Define a theme as a complete set of semantic tokens.
12
+ ```
13
+ {
14
+ "theme": {
15
+ "canonical": {
16
+ "color": {
17
+ "background": {
18
+ "default": {
19
+ "$type": "color",
20
+ "$value": "{color.neutral.100}"
21
+ }
22
+ }
23
+ }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ ### Don't
30
+
31
+ (Don't) Mix implementation details into theme definitions.
32
+ ```
33
+ {
34
+ "theme": {
35
+ "canonical": {
36
+ "class": "canonical", // Bad: CSS implementation detail
37
+ "container": "div", // Bad: HTML implementation detail
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ ---
44
+
45
+ ## styling/tokens/creation
46
+
47
+ Design tokens must be created for all design decisions in a component. See css/properties/values for how these tokens are used in CSS implementation.
48
+
49
+ ### Do
50
+
51
+ (Do) Create component tokens that reference semantic tokens for design decisions.
52
+ ```
53
+ {
54
+ "button": {
55
+ "background": {
56
+ "$type": "color",
57
+ "$value": "{color.background.primary}"
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ### Don't
64
+
65
+ (Don't) Use raw values for design decisions.
66
+ ```
67
+ {
68
+ "button": {
69
+ "background": {
70
+ "$type": "color",
71
+ "$value": "#0066CC" # Should reference semantic token like {color.background.primary}
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ ---
78
+
79
+ ## styling/tokens/scoping
80
+
81
+ Design tokens must be scoped according to their type:
82
+ - Primitive tokens: Global scope (system-wide base values)
83
+ - Semantic tokens: Theme scope (theme-specific bindings to primitive tokens)
84
+ - Component tokens: Component scope (bindings to semantic tokens)
85
+
86
+ ### Do
87
+
88
+ (Do) Define primitive tokens in global scope.
89
+ ```
90
+ {
91
+ "color": {
92
+ "neutral": {
93
+ "100": {
94
+ "$type": "color",
95
+ "$value": "#FFFFFF"
96
+ }
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### Don't
103
+
104
+ (Don't) Define primitive tokens in theme scope.
105
+ ```
106
+ {
107
+ "theme": {
108
+ "canonical": {
109
+ "color": {
110
+ "neutral": {
111
+ "100": {
112
+ "$type": "color",
113
+ "$value": "#FFFFFF" # Should be defined in global scope
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ ---
123
+
124
+ ## styling/tokens/types
125
+
126
+ Design tokens follow a strict hierarchy:
127
+ - Primitive tokens: Raw values that form the foundation of the design system
128
+ - Semantic tokens: Map semantic concepts to primitive token values
129
+ - Component tokens: Map component properties to semantic token values
130
+
131
+ ### Do
132
+
133
+ (Do) Define semantic tokens that map to primitive tokens.
134
+ ```
135
+ {
136
+ "color": {
137
+ "background": {
138
+ "default": {
139
+ "$type": "color",
140
+ "$value": "{color.neutral.100}"
141
+ }
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ ### Don't
148
+
149
+ (Don't) Skip the semantic layer by mapping component tokens directly to primitives.
150
+ ```
151
+ {
152
+ "button": {
153
+ "padding": {
154
+ "vertical": {
155
+ "$type": "dimension",
156
+ "$value": "{spacing.unit}" # Should reference semantic token like {spacing.vertical.medium}
157
+ }
158
+ }
159
+ }
160
+ }
161
+ ```
162
+
163
+ ---