|
| 1 | +--- |
| 2 | +name: component-with-design-system |
| 3 | +description: Guide for building React components using @stellar/design-system and project SCSS conventions. Invoke when creating new components or reviewing component code. |
| 4 | +--- |
| 5 | + |
| 6 | +# Component with Design System |
| 7 | + |
| 8 | +## Before Building Anything Custom |
| 9 | + |
| 10 | +Check if `@stellar/design-system` already has the component: |
| 11 | +https://github.com/stellar/stellar-design-system |
| 12 | + |
| 13 | +## Commonly Used Design System Components |
| 14 | + |
| 15 | +### Layout & Structure |
| 16 | +- `<Box gap="sm|md|lg">` — flex container with gap |
| 17 | +- `<Card>` — bordered card container |
| 18 | +- `<PageCard heading="...">` — card with heading, optional `rightElement` |
| 19 | +- `<PageHeader heading="..." as="h1|h2">` — page/section title |
| 20 | + |
| 21 | +### Form Inputs |
| 22 | +- `<Input>` — text input with label, error, note props |
| 23 | +- `<Select>` — dropdown with `fieldSize` prop |
| 24 | +- `<Textarea>` — multiline text |
| 25 | +- `<RadioButton id label fieldSize="sm|md|lg">` — radio input, extends native |
| 26 | + `<input>` attributes (`name`, `value`, `checked`, `onChange`, etc.) |
| 27 | + |
| 28 | +### Actions |
| 29 | +- `<Button variant="primary|secondary|tertiary|destructive">` — with optional |
| 30 | + `icon`, `isLoading`, `disabled` props |
| 31 | +- `<CopyText textToCopy={value}>` — text with copy button |
| 32 | + |
| 33 | +### Feedback |
| 34 | +- `<Alert variant="primary|success|warning|error" title="...">` — alert banner |
| 35 | + with optional children for body |
| 36 | +- `<Text size="xs|sm|md|lg" as="span|p|div">` — typography |
| 37 | +- `<Link>` — styled link |
| 38 | +- `<Icon.[Name] />` — icon from icon set |
| 39 | + |
| 40 | +### Display |
| 41 | +- `<Badge variant="primary|secondary|tertiary|success|warning|error" size="sm|md|lg">` |
| 42 | + — status label with optional `icon`, `iconPosition`, `isOutlined`, `isSquare`, |
| 43 | + `isStatus` props |
| 44 | +- `<Text size="xs|sm|md|lg" as="span|p|div">` — typography for displaying text |
| 45 | + |
| 46 | +## Component File Structure |
| 47 | + |
| 48 | +### With styles (preferred for non-trivial components) |
| 49 | + |
| 50 | +``` |
| 51 | +src/components/ComponentName/ |
| 52 | +├── index.tsx |
| 53 | +└── styles.scss |
| 54 | +``` |
| 55 | + |
| 56 | +### Without styles (simple components) |
| 57 | + |
| 58 | +``` |
| 59 | +src/components/ComponentName.tsx |
| 60 | +``` |
| 61 | + |
| 62 | +### Page-specific components |
| 63 | + |
| 64 | +``` |
| 65 | +src/app/(sidebar)/[feature]/components/ComponentName.tsx |
| 66 | +``` |
| 67 | + |
| 68 | +## Component Template |
| 69 | + |
| 70 | +```typescript |
| 71 | +"use client"; |
| 72 | + |
| 73 | +import { useState } from "react"; |
| 74 | +import { Button, Input, Card } from "@stellar/design-system"; |
| 75 | + |
| 76 | +import "./styles.scss"; |
| 77 | + |
| 78 | +interface ComponentNameProps { |
| 79 | + /** Description of prop */ |
| 80 | + propName: string; |
| 81 | +} |
| 82 | + |
| 83 | +/** |
| 84 | + * Brief description of what this component does |
| 85 | + * |
| 86 | + * @example |
| 87 | + * <ComponentName propName="value" /> |
| 88 | + */ |
| 89 | +export const ComponentName = ({ propName }: ComponentNameProps) => { |
| 90 | + return ( |
| 91 | + <div className="ComponentName"> |
| 92 | + <div className="ComponentName__header"> |
| 93 | + {/* ... */} |
| 94 | + </div> |
| 95 | + <div className="ComponentName__content"> |
| 96 | + {/* ... */} |
| 97 | + </div> |
| 98 | + </div> |
| 99 | + ); |
| 100 | +}; |
| 101 | +``` |
| 102 | + |
| 103 | +## SCSS Template |
| 104 | + |
| 105 | +```scss |
| 106 | +.ComponentName { |
| 107 | + // Root styles |
| 108 | + |
| 109 | + &__header { |
| 110 | + display: flex; |
| 111 | + align-items: center; |
| 112 | + justify-content: space-between; |
| 113 | + } |
| 114 | + |
| 115 | + &__content { |
| 116 | + display: flex; |
| 117 | + flex-direction: column; |
| 118 | + gap: pxToRem(16px); |
| 119 | + } |
| 120 | + |
| 121 | + &__footer { |
| 122 | + display: flex; |
| 123 | + gap: pxToRem(8px); |
| 124 | + } |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +## Conventions |
| 129 | + |
| 130 | +### Do |
| 131 | +- Use design system components for all standard UI elements |
| 132 | +- Use SCSS with BEM-ish class naming (`ComponentName__element--modifier`) |
| 133 | +- Use `pxToRem()` for spacing values in SCSS |
| 134 | +- Use SDS CSS custom properties for colors/fonts/gaps: `var(--sds-clr-gray-06)`, |
| 135 | + `var(--sds-ff-monospace)`, `var(--sds-gap-md)` |
| 136 | +- Use `data-*` attributes for state-driven styling instead of modifier classes: |
| 137 | + `data-is-active`, `data-is-visible`, `data-is-selected`, `data-is-clickable` |
| 138 | +- Use `data-testid` with dashes for test selectors (`data-testid="sign-button"`) |
| 139 | +- Add JSDoc comments with `@example` for exported components |
| 140 | +- Co-locate page-specific components with their page |
| 141 | +- Import shared SCSS utilities: `@use "../../styles/utils.scss" as *;` |
| 142 | + |
| 143 | +### Don't |
| 144 | +- Use inline `style={}` attributes — always use SCSS classes |
| 145 | +- Create custom buttons, inputs, or alerts when design system has equivalents |
| 146 | +- Use arbitrary hex colors — use SDS CSS custom properties (`--sds-clr-*`) |
| 147 | +- Put styles in the component file — always separate into `.scss` |
| 148 | +- Use `className` string concatenation — use template literals or classnames lib |
| 149 | +- Use conditional CSS classes for state — prefer `data-*` attributes |
| 150 | + |
| 151 | +## Design System Props Patterns |
| 152 | + |
| 153 | +### Input with validation |
| 154 | + |
| 155 | +```typescript |
| 156 | +<Input |
| 157 | + id="source-account" |
| 158 | + fieldSize="md" |
| 159 | + label="Source Account" |
| 160 | + value={sourceAccount} |
| 161 | + onChange={(e) => setSourceAccount(e.target.value)} |
| 162 | + error={getPublicKeyError(sourceAccount)} |
| 163 | + note="The account that originates the transaction" |
| 164 | +/> |
| 165 | +``` |
| 166 | + |
| 167 | +### Button with loading state |
| 168 | + |
| 169 | +```typescript |
| 170 | +<Button |
| 171 | + variant="primary" |
| 172 | + size="md" |
| 173 | + isLoading={isSubmitting} |
| 174 | + disabled={!isValid} |
| 175 | + onClick={handleSubmit} |
| 176 | +> |
| 177 | + Submit Transaction |
| 178 | +</Button> |
| 179 | +``` |
| 180 | + |
| 181 | +### Alert with details |
| 182 | + |
| 183 | +```typescript |
| 184 | +<Alert variant="warning" title="Auth entries require signing"> |
| 185 | + This transaction has {authCount} authorization entries that must be |
| 186 | + signed before the transaction envelope can be signed. |
| 187 | +</Alert> |
| 188 | +``` |
| 189 | + |
| 190 | +### RadioButton group |
| 191 | + |
| 192 | +```typescript |
| 193 | +<RadioButton |
| 194 | + id="network-testnet" |
| 195 | + name="network" |
| 196 | + label="Testnet" |
| 197 | + fieldSize="md" |
| 198 | + checked={network === "testnet"} |
| 199 | + onChange={() => setNetwork("testnet")} |
| 200 | +/> |
| 201 | +<RadioButton |
| 202 | + id="network-mainnet" |
| 203 | + name="network" |
| 204 | + label="Mainnet" |
| 205 | + fieldSize="md" |
| 206 | + checked={network === "mainnet"} |
| 207 | + onChange={() => setNetwork("mainnet")} |
| 208 | +/> |
| 209 | +``` |
| 210 | + |
| 211 | +### Badge with icon |
| 212 | + |
| 213 | +```typescript |
| 214 | +<Badge variant="success" size="sm" icon={<Icon.CheckCircle />} iconPosition="right"> |
| 215 | + Verified |
| 216 | +</Badge> |
| 217 | + |
| 218 | +<Badge variant="error">Failed</Badge> |
| 219 | + |
| 220 | +<Badge variant="secondary" size="sm"> |
| 221 | + {itemCount} items |
| 222 | +</Badge> |
| 223 | +``` |
| 224 | + |
| 225 | +### PageCard with action |
| 226 | + |
| 227 | +```typescript |
| 228 | +<PageCard |
| 229 | + heading="Transaction Parameters" |
| 230 | + rightElement={ |
| 231 | + <Button variant="tertiary" onClick={handleClear}> |
| 232 | + Clear all |
| 233 | + </Button> |
| 234 | + } |
| 235 | +> |
| 236 | + {/* Card content */} |
| 237 | +</PageCard> |
| 238 | +``` |
| 239 | + |
| 240 | +## Data Attribute Pattern for State-Driven Styling |
| 241 | + |
| 242 | +Prefer `data-*` attributes over CSS modifier classes for dynamic state: |
| 243 | + |
| 244 | +```typescript |
| 245 | +// Component |
| 246 | +<div |
| 247 | + className="TransactionStepper__step" |
| 248 | + data-is-active={isActive || undefined} |
| 249 | + data-is-completed={isCompleted || undefined} |
| 250 | + data-is-clickable={isClickable || undefined} |
| 251 | + onClick={isClickable ? () => onStepClick(step) : undefined} |
| 252 | +> |
| 253 | +``` |
| 254 | + |
| 255 | +```scss |
| 256 | +// SCSS — target with attribute selectors |
| 257 | +.TransactionStepper__step { |
| 258 | + opacity: 0.5; |
| 259 | + |
| 260 | + &[data-is-active="true"] { |
| 261 | + opacity: 1; |
| 262 | + font-weight: var(--sds-fw-medium); |
| 263 | + } |
| 264 | + |
| 265 | + &[data-is-clickable="true"] { |
| 266 | + cursor: pointer; |
| 267 | + } |
| 268 | + |
| 269 | + &[data-is-completed="true"] { |
| 270 | + opacity: 0.8; |
| 271 | + } |
| 272 | +} |
| 273 | +``` |
| 274 | + |
| 275 | +Pass `undefined` (not `false`) to omit the attribute from the DOM entirely. |
0 commit comments