@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.
- package/.mcp.json +8 -0
- package/README.md +297 -0
- package/data/code.ttl +437 -0
- package/data/css.ttl +265 -0
- package/data/icons.ttl +359 -0
- package/data/packaging.ttl +464 -0
- package/data/react.ttl +752 -0
- package/data/rust.ttl +1806 -0
- package/data/storybook.ttl +403 -0
- package/data/styling.ttl +165 -0
- package/data/tsdoc.ttl +216 -0
- package/data/turtle.ttl +179 -0
- package/definitions/CodeStandard.ttl +80 -0
- package/docs/code.md +720 -0
- package/docs/css.md +275 -0
- package/docs/icons.md +367 -0
- package/docs/index.md +15 -0
- package/docs/react.md +766 -0
- package/docs/rust.md +1784 -0
- package/docs/storybook.md +413 -0
- package/docs/styling.md +163 -0
- package/docs/tsdoc.md +213 -0
- package/docs/turtle.md +179 -0
- package/package.json +9 -0
- package/skills/add-standard/SKILL.md +288 -0
- package/src/scripts/generate-docs.ts +131 -0
- package/src/scripts/index.ts +19 -0
package/data/react.ttl
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
@prefix cs: <http://pragma.canonical.com/codestandards#> .
|
|
2
|
+
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
|
3
|
+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
4
|
+
@prefix owl: <http://www.w3.org/2002/07/owl#> .
|
|
5
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
6
|
+
|
|
7
|
+
# React Category
|
|
8
|
+
cs:ReactCategory a cs:Category ;
|
|
9
|
+
rdfs:label "React"@en ;
|
|
10
|
+
rdfs:comment "Standards specific to React development"@en ;
|
|
11
|
+
cs:slug "react" .
|
|
12
|
+
|
|
13
|
+
# Component TSDoc Standard
|
|
14
|
+
cs:ComponentTSDoc a cs:CodeStandard ;
|
|
15
|
+
cs:name "react/component/tsdoc" ;
|
|
16
|
+
cs:hasCategory cs:ReactCategory ;
|
|
17
|
+
cs:description """Component TSDoc documentation must use the description from the design system ontology (DSL). The TSDoc should NOT include @example blocks since stories serve as the examples. The description should be copied verbatim from the DSL, maintaining the original wording and meaning.""" ;
|
|
18
|
+
cs:dos """
|
|
19
|
+
(Do) Use the description from the DSL ontology verbatim.
|
|
20
|
+
```typescript
|
|
21
|
+
/**
|
|
22
|
+
* The label component is a compact, non-interactive visual element used to
|
|
23
|
+
* categorize content or indicate a status. Its primary role is metadata
|
|
24
|
+
* visualization. While it has similar visual properties to the Chip, it is
|
|
25
|
+
* purely informational and does not trigger actions or allow for removal.
|
|
26
|
+
*
|
|
27
|
+
* @implements dso:global.component.label
|
|
28
|
+
*/
|
|
29
|
+
const Label = ({ children, criticality, className, ...props }: LabelProps) => (
|
|
30
|
+
<span className={[componentCssClassName, criticality, className].filter(Boolean).join(" ")} {...props}>
|
|
31
|
+
{children}
|
|
32
|
+
</span>
|
|
33
|
+
);
|
|
34
|
+
```
|
|
35
|
+
""" ;
|
|
36
|
+
cs:donts """
|
|
37
|
+
(Don't) Write custom descriptions that deviate from the DSL.
|
|
38
|
+
```typescript
|
|
39
|
+
// Bad: Custom title and paraphrased description
|
|
40
|
+
/**
|
|
41
|
+
* Label component
|
|
42
|
+
*
|
|
43
|
+
* A compact visual element for status indication.
|
|
44
|
+
*/
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
(Don't) Include @example blocks - stories fulfill this role.
|
|
48
|
+
```typescript
|
|
49
|
+
// Bad: Examples belong in stories, not TSDoc
|
|
50
|
+
/**
|
|
51
|
+
* The label component is a compact...
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* <Label>Default</Label>
|
|
56
|
+
* <Label criticality="warning">Warning</Label>
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
(Don't) Omit the @implements tag that links to the DSL.
|
|
62
|
+
```typescript
|
|
63
|
+
// Bad: Missing @implements tag
|
|
64
|
+
/**
|
|
65
|
+
* The label component is a compact...
|
|
66
|
+
*/
|
|
67
|
+
```
|
|
68
|
+
""" .
|
|
69
|
+
|
|
70
|
+
# Component Folder Structure Standard
|
|
71
|
+
cs:ComponentFolderStructure a cs:CodeStandard ;
|
|
72
|
+
cs:name "react/component/structure/folder" ;
|
|
73
|
+
cs:extends cs:ComponentFolderStructure ;
|
|
74
|
+
cs:hasCategory cs:ReactCategory ;
|
|
75
|
+
cs:description "Each component must reside in its own folder, which contains all related files: component implementation, tests, type definitions, and optionally stories and styles. Some components may be styleless or may not have stories; in these cases, styles.css and [MyComponent].stories.tsx are optional. Context providers follow a different structure (see react/component/structure/context)." ;
|
|
76
|
+
cs:dos """
|
|
77
|
+
(Do) Place all component-related files within a single folder named after the component.
|
|
78
|
+
```bash
|
|
79
|
+
[MyComponent]/
|
|
80
|
+
├── [MyComponent].tsx
|
|
81
|
+
├── [MyComponent].stories.tsx
|
|
82
|
+
├── [MyComponent].test.tsx
|
|
83
|
+
├── index.ts
|
|
84
|
+
├── styles.css
|
|
85
|
+
└── types.ts
|
|
86
|
+
```
|
|
87
|
+
""" ;
|
|
88
|
+
cs:donts """
|
|
89
|
+
(Don't) Scatter component files across different parts of the application.
|
|
90
|
+
```bash
|
|
91
|
+
# Bad: Files are not co-located
|
|
92
|
+
components/
|
|
93
|
+
├── [MyComponent].tsx
|
|
94
|
+
stories/
|
|
95
|
+
└── [MyComponent].stories.tsx
|
|
96
|
+
styles/
|
|
97
|
+
└── [MyComponent].css
|
|
98
|
+
```
|
|
99
|
+
""" .
|
|
100
|
+
|
|
101
|
+
# Barrel Exports Standard
|
|
102
|
+
cs:BarrelExports a cs:CodeStandard ;
|
|
103
|
+
cs:name "react/component/barrel-exports" ;
|
|
104
|
+
cs:hasCategory cs:ReactCategory ;
|
|
105
|
+
cs:description "The index.ts file must be a complete barrel export for the component folder, re-exporting all public APIs." ;
|
|
106
|
+
cs:dos """
|
|
107
|
+
(Do) Create an index.ts file that re-exports all public APIs from the component folder.
|
|
108
|
+
```typescript
|
|
109
|
+
// index.ts
|
|
110
|
+
export { default as [MyComponent] } from './[MyComponent].js';
|
|
111
|
+
export type * from './types.js';
|
|
112
|
+
// If you have multiple components:
|
|
113
|
+
export { default as SubComponent } from './SubComponent.js';
|
|
114
|
+
```
|
|
115
|
+
""" ;
|
|
116
|
+
cs:donts """
|
|
117
|
+
(Don't) Omit the index.ts file or fail to re-export all public APIs.
|
|
118
|
+
```typescript
|
|
119
|
+
// Bad: index.ts only exports default, omits types and named exports
|
|
120
|
+
export { default } from './[MyComponent].js';
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
(Don't) Use `export * from './types.js'` as it allows value exports, which is not expected for types files.
|
|
124
|
+
```typescript
|
|
125
|
+
// Bad: Using export * from './types.js' allows value exports, which is not allowed
|
|
126
|
+
export * from './types.js';
|
|
127
|
+
```
|
|
128
|
+
""" .
|
|
129
|
+
|
|
130
|
+
# Component File Naming Standard
|
|
131
|
+
cs:ComponentFileNaming a cs:CodeStandard ;
|
|
132
|
+
cs:name "react/component/file-naming" ;
|
|
133
|
+
cs:hasCategory cs:ReactCategory ;
|
|
134
|
+
cs:description "Component folder and file naming is based on scope. Component-specific files (implementation, stories, tests) must be prefixed with the component's name (e.g., `MyComponent.tsx`, `MyComponent.stories.tsx`). Domain-level files that serve the entire folder (e.g., `Context.tsx`, `styles.css`, `types.ts`) should use generic, descriptive names, as the folder already provides the domain context." ;
|
|
135
|
+
cs:dos """
|
|
136
|
+
(Do) Prefix component-specific files and use generic names for domain-level files.
|
|
137
|
+
```
|
|
138
|
+
[MyComponent]/
|
|
139
|
+
├── [MyComponent].tsx # Component-specific
|
|
140
|
+
├── [MyComponent].stories.tsx # Component-specific
|
|
141
|
+
├── [MyComponent].test.tsx # Component-specific
|
|
142
|
+
├── Context.tsx # Domain-level
|
|
143
|
+
├── types.ts # Domain-level
|
|
144
|
+
└── styles.css # Domain-level
|
|
145
|
+
```
|
|
146
|
+
""" ;
|
|
147
|
+
cs:donts """
|
|
148
|
+
(Don't) Add redundant prefixes to domain-level files.
|
|
149
|
+
```
|
|
150
|
+
[MyComponent]/
|
|
151
|
+
├── [MyComponent].Context.tsx # Bad: Redundant prefix
|
|
152
|
+
├── [MyComponent].types.ts # Bad: Redundant prefix
|
|
153
|
+
└── [MyComponent].styles.css # Bad: Redundant prefix
|
|
154
|
+
```
|
|
155
|
+
""" .
|
|
156
|
+
|
|
157
|
+
# Component Props Standard
|
|
158
|
+
cs:ComponentProps a cs:CodeStandard ;
|
|
159
|
+
cs:name "react/component/props" ;
|
|
160
|
+
cs:hasCategory cs:ReactCategory ;
|
|
161
|
+
cs:description """Component props must:
|
|
162
|
+
- Be documented with TSDoc comments
|
|
163
|
+
- Be destructured when used in markup
|
|
164
|
+
- Be spread to the root element when unused
|
|
165
|
+
- Follow type-specific patterns based on what the component renders""" ;
|
|
166
|
+
cs:dos """
|
|
167
|
+
(Do) Document props with TSDoc comments and use proper destructuring and spreading.
|
|
168
|
+
```typescript
|
|
169
|
+
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
170
|
+
/** The button's text content */
|
|
171
|
+
label: string;
|
|
172
|
+
/** Optional icon to display before the label */
|
|
173
|
+
icon?: React.ReactNode;
|
|
174
|
+
/** Visual emphasis of the button */
|
|
175
|
+
emphasis?: ModifierFamily<"emphasis">;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const Button = ({
|
|
179
|
+
label,
|
|
180
|
+
icon,
|
|
181
|
+
emphasis,
|
|
182
|
+
className,
|
|
183
|
+
...props // Spread unused HTML button props
|
|
184
|
+
}: ButtonProps) => (
|
|
185
|
+
<button
|
|
186
|
+
className={[componentCssClassName, emphasis, className].filter(Boolean).join(" ")}
|
|
187
|
+
{...props}
|
|
188
|
+
>
|
|
189
|
+
{icon}
|
|
190
|
+
<span>{label}</span>
|
|
191
|
+
</button>
|
|
192
|
+
);
|
|
193
|
+
```
|
|
194
|
+
""" ;
|
|
195
|
+
cs:donts """
|
|
196
|
+
(Don't) Mix explicit and spread props or destructure props unnecessarily.
|
|
197
|
+
```typescript
|
|
198
|
+
// Bad: Mixing explicit props with spread
|
|
199
|
+
const Button = (props: ButtonProps) => (
|
|
200
|
+
<button
|
|
201
|
+
className={props.className}
|
|
202
|
+
onClick={props.onClick} // Should be in ...props
|
|
203
|
+
{...props} // Now duplicates onClick
|
|
204
|
+
>
|
|
205
|
+
{props.label}
|
|
206
|
+
</button>
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Bad: Unnecessarily destructuring HTML props
|
|
210
|
+
const Button = ({
|
|
211
|
+
label,
|
|
212
|
+
className,
|
|
213
|
+
onClick, // Should be in ...props
|
|
214
|
+
onFocus, // Should be in ...props
|
|
215
|
+
disabled, // Should be in ...props
|
|
216
|
+
...props
|
|
217
|
+
}: ButtonProps) => (
|
|
218
|
+
<button
|
|
219
|
+
className={className}
|
|
220
|
+
onClick={onClick} // Explicit when it could be spread
|
|
221
|
+
onFocus={onFocus} // Explicit when it could be spread
|
|
222
|
+
disabled={disabled} // Explicit when it could be spread
|
|
223
|
+
{...props}
|
|
224
|
+
>
|
|
225
|
+
{label}
|
|
226
|
+
</button>
|
|
227
|
+
);
|
|
228
|
+
```
|
|
229
|
+
""" .
|
|
230
|
+
|
|
231
|
+
# HTML Rendering Components Props Standard
|
|
232
|
+
cs:HTMLRenderingProps a cs:CodeStandard ;
|
|
233
|
+
cs:name "react/component/props/html-rendering" ;
|
|
234
|
+
cs:hasCategory cs:ReactCategory ;
|
|
235
|
+
cs:extends cs:ComponentProps ;
|
|
236
|
+
cs:description "Components that render HTML markup must extend the base HTML element props interface to enable passing native properties through spreading." ;
|
|
237
|
+
cs:dos """
|
|
238
|
+
(Do) Extend the appropriate React HTML props interface and add component-specific props.
|
|
239
|
+
```typescript
|
|
240
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
241
|
+
/** The button label */
|
|
242
|
+
label: string;
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
""" ;
|
|
246
|
+
cs:donts """
|
|
247
|
+
(Don't) Manually redefine standard HTML attributes that are already available through the base interface.
|
|
248
|
+
```typescript
|
|
249
|
+
export interface ButtonProps {
|
|
250
|
+
/** The button label */
|
|
251
|
+
label: string;
|
|
252
|
+
onClick?: () => void; // Bad: Duplicates HTML button props
|
|
253
|
+
disabled?: boolean; // Bad: Duplicates HTML button props
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
""" .
|
|
257
|
+
|
|
258
|
+
# Component Naming Standard
|
|
259
|
+
cs:ComponentNaming a cs:CodeStandard ;
|
|
260
|
+
cs:name "react/component/naming" ;
|
|
261
|
+
cs:hasCategory cs:ReactCategory ;
|
|
262
|
+
cs:description "Components must use PascalCase naming and be descriptive of their purpose." ;
|
|
263
|
+
cs:dos """
|
|
264
|
+
(Do) Use PascalCase and descriptive names for components:
|
|
265
|
+
UserProfile
|
|
266
|
+
NavigationBar
|
|
267
|
+
SearchResultList
|
|
268
|
+
""" ;
|
|
269
|
+
cs:donts """
|
|
270
|
+
(Don't) Use non-PascalCase or unclear names:
|
|
271
|
+
userProfile
|
|
272
|
+
navigation_bar
|
|
273
|
+
searchresultlist
|
|
274
|
+
""" .
|
|
275
|
+
|
|
276
|
+
# Hook Naming Standard
|
|
277
|
+
cs:HookNaming a cs:CodeStandard ;
|
|
278
|
+
cs:name "react/hooks/naming" ;
|
|
279
|
+
cs:hasCategory cs:ReactCategory ;
|
|
280
|
+
cs:description "The hook name must start with 'use' and clearly describe its purpose." ;
|
|
281
|
+
cs:dos """
|
|
282
|
+
(Do) Name custom hooks so the hook name starts with 'use' and is descriptive:
|
|
283
|
+
```typescript
|
|
284
|
+
useWindowSize()
|
|
285
|
+
useAuthentication()
|
|
286
|
+
useFormValidation()
|
|
287
|
+
```
|
|
288
|
+
""" ;
|
|
289
|
+
cs:donts """
|
|
290
|
+
(Don't) Name hooks without the 'use' prefix at the start of the hook name:
|
|
291
|
+
```typescript
|
|
292
|
+
windowSize()
|
|
293
|
+
getAuth()
|
|
294
|
+
formValidation()
|
|
295
|
+
```
|
|
296
|
+
""" .
|
|
297
|
+
|
|
298
|
+
# Component Dependencies Standard
|
|
299
|
+
cs:ComponentDependencies a cs:CodeStandard ;
|
|
300
|
+
cs:name "react/component/dependencies" ;
|
|
301
|
+
cs:hasCategory cs:ReactCategory ;
|
|
302
|
+
cs:description """Component dependencies must follow a strict unidirectional flow:
|
|
303
|
+
- Subcomponents must be in a `common/` folder within the parent component's directory
|
|
304
|
+
- Dependencies can flow downwards (parent to subcomponent) or sideways (between siblings)
|
|
305
|
+
- Dependencies must not flow upwards (subcomponent to parent)""" ;
|
|
306
|
+
cs:dos """
|
|
307
|
+
(Do) Place subcomponents in a `common/` folder inside the parent component directory.
|
|
308
|
+
```
|
|
309
|
+
Card/
|
|
310
|
+
├── Card.tsx
|
|
311
|
+
├── common/
|
|
312
|
+
│ ├── Header/
|
|
313
|
+
│ │ └── Header.tsx
|
|
314
|
+
│ ├── Footer/
|
|
315
|
+
│ │ └── Footer.tsx
|
|
316
|
+
│ └── utils/
|
|
317
|
+
│ └── helpers.ts
|
|
318
|
+
└── index.ts
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
(Do) Allow subcomponents to depend on siblings or shared utilities within the same component scope.
|
|
322
|
+
```typescript
|
|
323
|
+
// Header.tsx can import from utils/
|
|
324
|
+
import { helper } from '../utils/helpers.js';
|
|
325
|
+
|
|
326
|
+
// Footer.tsx can import from Header.tsx
|
|
327
|
+
import Header from '../Header.js';
|
|
328
|
+
```
|
|
329
|
+
""" ;
|
|
330
|
+
cs:donts """
|
|
331
|
+
(Don't) Create dependencies that flow upwards from a subcomponent to its parent.
|
|
332
|
+
```typescript
|
|
333
|
+
// Bad: Header.tsx in Card/common/ should not import from Card.tsx
|
|
334
|
+
import Card from '../../Card.js';
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
(Don't) Allow external components to depend on the internal structure of another component.
|
|
338
|
+
```typescript
|
|
339
|
+
// Bad: AnotherComponent should not import from Card's internal common folder
|
|
340
|
+
import Header from '../Card/common/Header.js';
|
|
341
|
+
```
|
|
342
|
+
""" .
|
|
343
|
+
|
|
344
|
+
# Subcomponents Export and Consumption API Standard
|
|
345
|
+
cs:SubcomponentsExportAPI a cs:CodeStandard ;
|
|
346
|
+
cs:name "react/component/subcomponent-export-api" ;
|
|
347
|
+
cs:hasCategory cs:ReactCategory ;
|
|
348
|
+
cs:description """Subcomponents are **public** if they are intended to be directly used by the consumer of the parent component to construct a UI.
|
|
349
|
+
|
|
350
|
+
Subcomponents are **private** if they are internal implementation details of the parent component, and are not intended for direct use by the consumer.
|
|
351
|
+
|
|
352
|
+
Public subcomponents must be:
|
|
353
|
+
- Exported by attaching them to the parent component using dot notation.
|
|
354
|
+
- Named semantically.
|
|
355
|
+
- Kept to a single level of nesting.
|
|
356
|
+
|
|
357
|
+
Private subcomponents must remain internal to the component's implementation and not be exported by any file.""" ;
|
|
358
|
+
cs:dos """
|
|
359
|
+
(Do) Export public subcomponents by attaching them to the parent component using dot notation:
|
|
360
|
+
```typescript
|
|
361
|
+
const Item = (props: ItemProps) => { /* ... */ };
|
|
362
|
+
const Accordion = (props: AccordionProps) => { /* ... */ };
|
|
363
|
+
Accordion.Item = Item;
|
|
364
|
+
export default Accordion;
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
(Do) Use semantic, self-descriptive names for subcomponents:
|
|
368
|
+
```typescript
|
|
369
|
+
Accordion.Item
|
|
370
|
+
Card.Header
|
|
371
|
+
Card.Footer
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
(Do) Keep subcomponent nesting to a single level:
|
|
375
|
+
```typescript
|
|
376
|
+
<Card>
|
|
377
|
+
<Card.Header />
|
|
378
|
+
<Card.Footer />
|
|
379
|
+
</Card>
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
""" ;
|
|
383
|
+
cs:donts """
|
|
384
|
+
(Don't) Repeat the parent component name in subcomponent names:
|
|
385
|
+
```typescript
|
|
386
|
+
Card.CardHeader = Header; // Bad: Redundant 'Card' prefix
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
(Don't) Map a subcomponent to a different name (renaming):
|
|
390
|
+
```typescript
|
|
391
|
+
Card.Top = Header; // Bad: Mapping 'Header' to 'Top' is not allowed
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
(Don't) Use non-semantic or unclear subcomponent names:
|
|
395
|
+
```
|
|
396
|
+
Card/
|
|
397
|
+
└── common/
|
|
398
|
+
├── Part/ # Bad: Too vague, not semantic - what part?
|
|
399
|
+
├── Element/ # Bad: Too vague, not semantic - what element?
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
(Don't) Nest subcomponents more than one level deep:
|
|
403
|
+
```typescript
|
|
404
|
+
<Card>
|
|
405
|
+
<Card.Header>
|
|
406
|
+
<Card.Header.Title />
|
|
407
|
+
</Card.Header>
|
|
408
|
+
</Card>
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
(Don't) Export private subcomponents that are not intended for public use.
|
|
412
|
+
```typescript
|
|
413
|
+
// Bad: Exporting internal-only subcomponents
|
|
414
|
+
export { InternalHelper };
|
|
415
|
+
```
|
|
416
|
+
""" .
|
|
417
|
+
|
|
418
|
+
# CSS ClassName Construction Standard
|
|
419
|
+
cs:ClassNameConstruction a cs:CodeStandard ;
|
|
420
|
+
cs:name "react/component/class-name-construction" ;
|
|
421
|
+
cs:hasCategory cs:ReactCategory ;
|
|
422
|
+
cs:description """Component CSS class names must be constructed following a specific pattern:
|
|
423
|
+
|
|
424
|
+
1. Base Class Constant: A `componentCssClassName` constant must be defined at the top of the component file. This constant holds the component's base class name, including the global `ds` scope (e.g., `"ds button"`).
|
|
425
|
+
2. Array Construction: The `className` string must be built from an array of classes.
|
|
426
|
+
3. Class Order: The classes in the array must be ordered from least to most specific to ensure a predictable CSS cascade:
|
|
427
|
+
a. Base Class: The `componentCssClassName` constant.
|
|
428
|
+
b. Modifier Classes: Classes derived from component props (e.g., `emphasis`, `severity`).
|
|
429
|
+
c. Consumer Classes: The `className` prop passed by the consumer.
|
|
430
|
+
4. Filtering and Joining: The array must be processed with `.filter(Boolean).join(" ")` to remove any falsy values (e.g., undefined props, expressions that evaluate to false) and create the final space-delimited string.""" ;
|
|
431
|
+
cs:dos """
|
|
432
|
+
(Do) Follow the complete pattern for class name construction.
|
|
433
|
+
```tsx
|
|
434
|
+
const componentCssClassName = "ds badge";
|
|
435
|
+
|
|
436
|
+
const Badge = ({
|
|
437
|
+
value,
|
|
438
|
+
className,
|
|
439
|
+
severity,
|
|
440
|
+
...props
|
|
441
|
+
}: BadgeProps): React.ReactElement => {
|
|
442
|
+
return (
|
|
443
|
+
<span
|
|
444
|
+
className={[componentCssClassName, severity, className]
|
|
445
|
+
.filter(Boolean)
|
|
446
|
+
.join(" ")}
|
|
447
|
+
{...props}
|
|
448
|
+
>
|
|
449
|
+
{value}
|
|
450
|
+
</span>
|
|
451
|
+
);
|
|
452
|
+
};
|
|
453
|
+
```
|
|
454
|
+
""" ;
|
|
455
|
+
cs:donts """
|
|
456
|
+
(Don't) Hardcode the base class name inside the JSX.
|
|
457
|
+
```tsx
|
|
458
|
+
// Bad: Base class "ds badge" is hardcoded.
|
|
459
|
+
<span className={["ds badge", severity, className].filter(Boolean).join(" ")}>
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
(Don't) Place the consumer `className` prop before other classes.
|
|
463
|
+
```tsx
|
|
464
|
+
// Bad: Consumer class is first
|
|
465
|
+
<span className={[className, componentCssClassName, severity].filter(Boolean).join(" ")}>
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
(Don't) Use string concatenation or template literals to add class names.
|
|
469
|
+
```tsx
|
|
470
|
+
// Bad: Harder to read and maintain, vulnerable to inconsistent formatting
|
|
471
|
+
<span className={`${componentCssClassName} ${severity} ${className}`}>
|
|
472
|
+
```
|
|
473
|
+
""" .
|
|
474
|
+
|
|
475
|
+
# Wrapper Component Props Standard
|
|
476
|
+
cs:WrapperComponentProps a cs:CodeStandard ;
|
|
477
|
+
cs:name "react/component/props/wrapper" ;
|
|
478
|
+
cs:hasCategory cs:ReactCategory ;
|
|
479
|
+
cs:extends cs:ComponentProps ;
|
|
480
|
+
cs:description "Wrapper components must use namespaced props for inner components and accept unscoped props for the wrapper element." ;
|
|
481
|
+
cs:dos """
|
|
482
|
+
(Do) Use namespaced props for inner components and unscoped props for the wrapper element.
|
|
483
|
+
```tsx
|
|
484
|
+
interface ThumbnailSectionProps extends SectionProps {
|
|
485
|
+
/** Props for the thumbnail image */
|
|
486
|
+
imageProps: Omit<React.ImgHTMLAttributes<HTMLImageElement>, "alt"> & {
|
|
487
|
+
alt: string;
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const ThumbnailSection = ({
|
|
492
|
+
imageProps,
|
|
493
|
+
className,
|
|
494
|
+
...props
|
|
495
|
+
}: ThumbnailSectionProps) => (
|
|
496
|
+
<Section className={[componentCssClassName, className].filter(Boolean).join(" ")} {...props}>
|
|
497
|
+
<img {...imageProps} />
|
|
498
|
+
</Section>
|
|
499
|
+
);
|
|
500
|
+
```
|
|
501
|
+
""" ;
|
|
502
|
+
cs:donts """
|
|
503
|
+
(Don't) Mix prop scopes between wrapper and inner components.
|
|
504
|
+
```tsx
|
|
505
|
+
interface ThumbnailSectionProps {
|
|
506
|
+
src: string; // Bad: Unscoped image props
|
|
507
|
+
alt: string; // Bad: Unscoped image props
|
|
508
|
+
width: number; // Bad: Ambiguous - for image or section?
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const ThumbnailSection = ({ src, alt, width, ...props }: ThumbnailSectionProps) => (
|
|
512
|
+
<Section {...props}>
|
|
513
|
+
<img src={src} alt={alt} width={width} />
|
|
514
|
+
</Section>
|
|
515
|
+
);
|
|
516
|
+
```
|
|
517
|
+
""" .
|
|
518
|
+
|
|
519
|
+
# Custom Hooks Standard
|
|
520
|
+
cs:CustomHooks a cs:CodeStandard ;
|
|
521
|
+
cs:name "react/hooks/custom" ;
|
|
522
|
+
cs:hasCategory cs:ReactCategory ;
|
|
523
|
+
cs:description """Custom hooks must separate concerns at the domain level. All hooks within a ComponentDomain/hooks directory are considered within the domain scope and should focus on a single concern. Custom hooks must be used when:
|
|
524
|
+
- Logic needs to be shared between components
|
|
525
|
+
- Component logic becomes complex
|
|
526
|
+
- State management needs to be abstracted
|
|
527
|
+
- Side effects need to be encapsulated
|
|
528
|
+
|
|
529
|
+
All of the types for a domain level's hooks must be defined in the `hooks/types.ts` file of that folder.
|
|
530
|
+
Each hook must define a [HookName]Props and [HookName]Result type in `hooks/types.ts`.""" ;
|
|
531
|
+
cs:dos """
|
|
532
|
+
(Do) Create a custom hook that focuses on a single concern within the domain.
|
|
533
|
+
```typescript
|
|
534
|
+
// [MyComponent]/hooks/useWindowFitment.ts
|
|
535
|
+
const useWindowFitment = ({
|
|
536
|
+
onBestPositionChange,
|
|
537
|
+
autoFit = false,
|
|
538
|
+
}: UseWindowFitmentProps): UseWindowFitmentResult => {
|
|
539
|
+
```
|
|
540
|
+
(Do) Create hook types in `hooks/types.ts`.
|
|
541
|
+
```typescript
|
|
542
|
+
// [MyComponent]/hooks/types.ts
|
|
543
|
+
export interface UseWindowFitmentProps {
|
|
544
|
+
/**
|
|
545
|
+
* Whether the popup should automatically fit into the viewport.
|
|
546
|
+
* If true, the hook will try to fit the popup into the viewport if it doesn't fit in the preferred directions.
|
|
547
|
+
* Defaults to false.
|
|
548
|
+
*/
|
|
549
|
+
autoFit?: boolean;
|
|
550
|
+
/**
|
|
551
|
+
* An optional callback to be called when the best position of the popup changes.
|
|
552
|
+
*/
|
|
553
|
+
onBestPositionChange?: (bestPosition?: BestPosition) => void;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export interface UseWindowFitmentResult {
|
|
557
|
+
/**
|
|
558
|
+
* A ref to be attached to the target element.
|
|
559
|
+
*/
|
|
560
|
+
targetRef: RefObject<HTMLDivElement | null>;
|
|
561
|
+
/**
|
|
562
|
+
* A ref to be attached to the popup element.
|
|
563
|
+
*/
|
|
564
|
+
popupRef: RefObject<HTMLDivElement | null>;
|
|
565
|
+
/**
|
|
566
|
+
* The calculated best possible position of the popup element.
|
|
567
|
+
*/
|
|
568
|
+
bestPosition?: BestPosition;
|
|
569
|
+
/**
|
|
570
|
+
* The style object to be applied to the popup element.
|
|
571
|
+
*/
|
|
572
|
+
popupPositionStyle: CSSProperties;
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
""" ;
|
|
577
|
+
cs:donts """
|
|
578
|
+
(Don't) Create a custom hook for simple, non-reusable state.
|
|
579
|
+
```tsx
|
|
580
|
+
// Bad: Unnecessary abstraction for a simple counter
|
|
581
|
+
const useCounter = () => {
|
|
582
|
+
const [count, setCount] = useState(0);
|
|
583
|
+
return { count, setCount };
|
|
584
|
+
};
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
(Don't) Mix multiple concerns in a single hook
|
|
588
|
+
```typescript
|
|
589
|
+
// Bad: Multiple concerns in one hook
|
|
590
|
+
const useUserData = () => {
|
|
591
|
+
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
592
|
+
const [profile, setProfile] = useState(null);
|
|
593
|
+
const [settings, setSettings] = useState({});
|
|
594
|
+
const [notifications, setNotifications] = useState([]);
|
|
595
|
+
return { isAuthenticated, profile, settings, notifications };
|
|
596
|
+
};
|
|
597
|
+
```
|
|
598
|
+
""" .
|
|
599
|
+
|
|
600
|
+
# Context Provider Structure Standard
|
|
601
|
+
cs:ContextProviderStructure a cs:CodeStandard ;
|
|
602
|
+
cs:name "react/component/structure/context" ;
|
|
603
|
+
cs:extends cs:ComponentFolderStructure ;
|
|
604
|
+
cs:hasCategory cs:ReactCategory ;
|
|
605
|
+
cs:description "Context providers must use `Provider.tsx` as the main component file instead of the standard component naming pattern `[MyComponent].tsx`. Provider prop and return types must be defined in the hook-level types file `hooks/types.ts` with `UseProviderStateProps` (containing all `ProviderProps` minus `children`) and `UseProviderStateResult` (matching the context options)." ;
|
|
606
|
+
cs:dos """
|
|
607
|
+
(Do) Organize provider-related files by separating concerns into distinct files: place the context definition in `Context.tsx`, the provider implementation in `Provider.tsx`, and provider-specific hooks in a `hooks/` directory. Create a `hooks/useProviderState.ts` file to centrally manage the state of the provider.`
|
|
608
|
+
```
|
|
609
|
+
[MyComponent]/
|
|
610
|
+
├── Context.tsx
|
|
611
|
+
├── Provider.tsx
|
|
612
|
+
├── index.ts
|
|
613
|
+
├── types.ts
|
|
614
|
+
└── common/
|
|
615
|
+
└─ [SubComponent]/
|
|
616
|
+
├── index.ts
|
|
617
|
+
├── [SubComponent].tsx
|
|
618
|
+
└── types.ts
|
|
619
|
+
└── hooks/
|
|
620
|
+
├── index.ts
|
|
621
|
+
├── types.ts
|
|
622
|
+
└── useProviderState.ts
|
|
623
|
+
```
|
|
624
|
+
(Do) Create the context type within `types.ts`.
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
// [MyComponent]/types.ts
|
|
628
|
+
/** The value of the config context */
|
|
629
|
+
export interface ContextOptions {
|
|
630
|
+
/** Whether the baseline grid should be shown */
|
|
631
|
+
showBaselineGrid: boolean;
|
|
632
|
+
/** Toggles the baseline grid's visibility. */
|
|
633
|
+
toggleShowBaselineGrid: () => void;
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
(Do) create the provider props type within `types.ts`, accepting `children` at a minimum and more props as needed.
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
// [MyComponent]/types.ts
|
|
641
|
+
|
|
642
|
+
export interface ProviderProps {
|
|
643
|
+
// The child nodes which will have access to the provider state
|
|
644
|
+
children: React.ReactNode;
|
|
645
|
+
// ...other props...
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
(Do) Create a `Context.tsx` file for the context definition.
|
|
650
|
+
```tsx
|
|
651
|
+
// [MyComponent]/Context.tsx
|
|
652
|
+
import { createContext } from "react";
|
|
653
|
+
import type { ContextOptions } from "./types.js";
|
|
654
|
+
|
|
655
|
+
const Context = createContext<ContextOptions | undefined>(undefined);
|
|
656
|
+
|
|
657
|
+
export default Context;
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
(Do) Use `Provider.tsx` as the main component file. The provider is responsible for wrapping children with the context.
|
|
662
|
+
```tsx
|
|
663
|
+
// [MyComponent]/Provider.tsx
|
|
664
|
+
import Context from "./Context.js";
|
|
665
|
+
import { useProviderState } from "./hooks/useProviderState.js";
|
|
666
|
+
import type { ProviderProps } from "./types.js";
|
|
667
|
+
|
|
668
|
+
const Provider = ({ children }: ProviderProps) => {
|
|
669
|
+
const state = useProviderState();
|
|
670
|
+
return <Context.Provider value={state}>{children}</Context.Provider>;
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
export default Provider;
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
(Do) Use `index.ts` to export the `Provider` as a named component that matches the folder name, casting it to the component type.
|
|
677
|
+
```typescript
|
|
678
|
+
// [MyComponent]/types.ts
|
|
679
|
+
import type { ReactElement } from "react";
|
|
680
|
+
|
|
681
|
+
export type [MyComponent]Component = ((props: ProviderProps) => ReactElement) & {
|
|
682
|
+
SubComponent: (props: SubComponentProps) => ReactElement | null;
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// [MyComponent]/index.ts
|
|
686
|
+
import Provider from "./Provider.js";
|
|
687
|
+
import type { [MyComponent] } from "./types.js";
|
|
688
|
+
|
|
689
|
+
export const [MyComponent] = Provider as [MyComponent]Component;
|
|
690
|
+
export default [MyComponent];
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
(Do) Create provider state hook types in `hooks/types.ts` for context providers.
|
|
694
|
+
```typescript
|
|
695
|
+
// [MyComponent]/hooks/types.ts
|
|
696
|
+
import type { ContextOptions, ProviderProps } from "../types.js";
|
|
697
|
+
|
|
698
|
+
// The props expected by the provider state hook. The children prop is omitted, as it is not passed to the hook - it is only rendered by the provider component.
|
|
699
|
+
export type UseProviderStateProps = Omit<ProviderProps, "children">;
|
|
700
|
+
|
|
701
|
+
// The result of the provider state hook. This should match the context options defined in the main component types file.
|
|
702
|
+
export type UseProviderStateResult = ContextOptions;
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
(Do) Create a provider state hook implementation.
|
|
706
|
+
```tsx
|
|
707
|
+
// [MyComponent]/hooks/useProviderState.ts
|
|
708
|
+
import { useContext } from "react";
|
|
709
|
+
import type { UseProviderStateProps, UseProviderStateResult } from "./types.js";
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Hook to manage the state of the provider
|
|
713
|
+
*/
|
|
714
|
+
const useProviderState = ({
|
|
715
|
+
// ..props...
|
|
716
|
+
}: UseProviderStateProps): UseProviderStateResult => {
|
|
717
|
+
// centralize the entire provider state here...
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
""" ;
|
|
721
|
+
cs:donts """
|
|
722
|
+
(Don't) Create a separate component file (e.g., `[MyComponent].tsx`) when `Provider.tsx` exists. The provider is the main component.
|
|
723
|
+
```
|
|
724
|
+
[MyComponent]/
|
|
725
|
+
├── [MyComponent].tsx
|
|
726
|
+
└── Provider.tsx
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
(Don't) Nest context-related files in a `context/` subfolder.
|
|
730
|
+
```
|
|
731
|
+
[MyComponent]/
|
|
732
|
+
└── context/ # Unnecessary nesting
|
|
733
|
+
├── Context.tsx
|
|
734
|
+
└── Provider.tsx
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
(Don't) Create multiple provider files within the same component folder.
|
|
738
|
+
```
|
|
739
|
+
[MyComponent]/
|
|
740
|
+
├── Provider.tsx
|
|
741
|
+
└── AnotherProvider.tsx # Only one provider per component
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
(Don't) Mix concerns of the Provider and its state.
|
|
745
|
+
|
|
746
|
+
```tsx
|
|
747
|
+
exort const Provider = ({ children }: ProviderProps) => {
|
|
748
|
+
const [state, setState] = useState(...); // Bad: State logic mixed in
|
|
749
|
+
return <Context.Provider value={{ state, setState }}>{children}</Context.Provider>;
|
|
750
|
+
};
|
|
751
|
+
```
|
|
752
|
+
""" .
|