Skip to content

Commit 9dc367b

Browse files
Merge pull request #190 from richardthe3rd/claude/festival-linking-phase-zero-k3Rpl
Review and implement festival linking phase zero
2 parents 790aa92 + ac893b5 commit 9dc367b

11 files changed

Lines changed: 1743 additions & 0 deletions

File tree

docs/code/widget-standards.md

Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
# Widget Coding Standards
2+
3+
Coding standards for Flutter widgets in the Cambridge Beer Festival app.
4+
5+
## 📝 Text Selectability
6+
7+
**Standard:** Content text must be selectable for better UX and accessibility.
8+
9+
### Use `SelectableText` for:
10+
11+
-**Content text**: Drink names, brewery names, descriptions
12+
-**Data values**: ABV, ratings, styles, categories
13+
-**User-generated content**: Reviews, notes, tasting notes
14+
-**Long-form text**: Descriptions, festival info, about text
15+
-**Informational text**: Any text users might want to copy
16+
17+
### Use regular `Text` for:
18+
19+
-**UI labels**: "Filter by:", "Sort by:", etc.
20+
-**Button text**: Text inside buttons or interactive elements
21+
-**Navigation elements**: Breadcrumbs, tabs, menu items
22+
-**Short helper text**: Decorative or instructional text
23+
24+
### Examples
25+
26+
```dart
27+
// ✅ GOOD - Selectable content
28+
SelectableText(
29+
drink.name,
30+
style: Theme.of(context).textTheme.titleLarge,
31+
)
32+
33+
// ✅ GOOD - Selectable data
34+
SelectableText(
35+
'${drink.abv}% ABV',
36+
style: TextStyle(fontWeight: FontWeight.bold),
37+
)
38+
39+
// ✅ GOOD - Selectable description
40+
SelectableText(
41+
drink.description,
42+
maxLines: 3,
43+
style: Theme.of(context).textTheme.bodyMedium,
44+
)
45+
46+
// ❌ GOOD - Non-selectable UI label
47+
Text('Filter by:') // Just a label, not content
48+
49+
// ❌ GOOD - Non-selectable navigation
50+
Text(backLabel) // Part of navigation control
51+
```
52+
53+
### Testing
54+
55+
Always verify text selectability in widget tests:
56+
57+
```dart
58+
testWidgets('drink name is selectable', (tester) async {
59+
await tester.pumpWidget(
60+
MaterialApp(
61+
home: Scaffold(
62+
body: SelectableText('Sample IPA'),
63+
),
64+
),
65+
);
66+
67+
// Verify SelectableText is used
68+
expect(find.byType(SelectableText), findsOneWidget);
69+
});
70+
```
71+
72+
---
73+
74+
## ♿ Accessibility Requirements
75+
76+
**See [`accessibility.md`](accessibility.md) for comprehensive accessibility standards.**
77+
78+
### Quick Checklist for Widgets
79+
80+
- [ ] All interactive elements have `Semantics` labels
81+
- [ ] `Semantics` only wraps interactive elements (not decorative text)
82+
- [ ] Touch targets are at least 48x48 pixels
83+
- [ ] Color contrast meets WCAG AA standards (4.5:1)
84+
- [ ] Text scales properly (test at 200%)
85+
- [ ] No reliance on color alone for information
86+
87+
### Semantics Pattern
88+
89+
```dart
90+
// ✅ GOOD - Only button has Semantics
91+
Semantics(
92+
label: 'Add to favorites',
93+
button: true,
94+
child: IconButton(
95+
icon: Icon(Icons.favorite_border),
96+
onPressed: () => addToFavorites(),
97+
),
98+
)
99+
100+
// ❌ BAD - Entire row marked as button when only icon is tappable
101+
Semantics(
102+
label: 'Drink card',
103+
button: true,
104+
child: Row(
105+
children: [
106+
IconButton(...), // Only this is tappable
107+
Text(...), // Not tappable but included in button semantics
108+
],
109+
),
110+
)
111+
```
112+
113+
---
114+
115+
## 🎨 Widget Patterns
116+
117+
### Text Overflow Handling
118+
119+
Always specify overflow behavior for constrained text:
120+
121+
```dart
122+
// ✅ GOOD - Explicit overflow handling
123+
Text(
124+
longText,
125+
overflow: TextOverflow.ellipsis,
126+
maxLines: 1,
127+
)
128+
129+
// ✅ GOOD - Multi-line with fade
130+
Text(
131+
description,
132+
overflow: TextOverflow.fade,
133+
maxLines: 3,
134+
)
135+
136+
// ❌ BAD - No overflow handling (can cause layout issues)
137+
Text(longText)
138+
```
139+
140+
### Const Constructors
141+
142+
Use `const` wherever possible for performance:
143+
144+
```dart
145+
// ✅ GOOD
146+
const Text('Label')
147+
const SizedBox(height: 16)
148+
const Icon(Icons.star)
149+
150+
// ❌ BAD
151+
Text('Label')
152+
SizedBox(height: 16)
153+
Icon(Icons.star)
154+
```
155+
156+
### Widget Organization
157+
158+
```dart
159+
class MyWidget extends StatelessWidget {
160+
const MyWidget({
161+
required this.title,
162+
this.subtitle,
163+
super.key,
164+
});
165+
166+
// Required parameters first
167+
final String title;
168+
169+
// Optional parameters after
170+
final String? subtitle;
171+
172+
@override
173+
Widget build(BuildContext context) {
174+
return Column(
175+
children: [
176+
_buildHeader(),
177+
_buildContent(),
178+
],
179+
);
180+
}
181+
182+
// Extract complex widgets into methods
183+
Widget _buildHeader() {
184+
return Text(title);
185+
}
186+
187+
Widget _buildContent() {
188+
return Text(subtitle ?? '');
189+
}
190+
}
191+
```
192+
193+
---
194+
195+
## 🧪 Testing Requirements
196+
197+
### Every widget must have tests for:
198+
199+
1. **Rendering**: Widget renders without errors
200+
2. **Content**: Expected text/icons appear
201+
3. **Interaction**: Buttons/taps trigger callbacks
202+
4. **Accessibility**: Semantics labels are correct
203+
5. **Edge cases**: Long text, Unicode, empty states
204+
205+
### Test Template
206+
207+
```dart
208+
testWidgets('MyWidget displays content correctly', (tester) async {
209+
await tester.pumpWidget(
210+
MaterialApp(
211+
home: Scaffold(
212+
body: MyWidget(title: 'Test'),
213+
),
214+
),
215+
);
216+
217+
// Test rendering
218+
expect(find.byType(MyWidget), findsOneWidget);
219+
220+
// Test content
221+
expect(find.text('Test'), findsOneWidget);
222+
223+
// Test interaction (if applicable)
224+
await tester.tap(find.byIcon(Icons.close));
225+
expect(onCloseCalled, isTrue);
226+
227+
// Test accessibility
228+
final semantics = tester.widget<Semantics>(find.byType(Semantics));
229+
expect(semantics.properties.label, 'Close button');
230+
});
231+
```
232+
233+
---
234+
235+
## 📏 Code Style
236+
237+
### Imports
238+
239+
```dart
240+
// Flutter SDK imports first
241+
import 'package:flutter/material.dart';
242+
import 'package:flutter/services.dart';
243+
244+
// Package imports second
245+
import 'package:provider/provider.dart';
246+
247+
// Local imports last
248+
import 'package:cambridge_beer_festival/models/models.dart';
249+
import 'package:cambridge_beer_festival/widgets/widgets.dart';
250+
```
251+
252+
### Strings
253+
254+
Always use single quotes:
255+
256+
```dart
257+
// ✅ GOOD
258+
const text = 'Hello';
259+
260+
// ❌ BAD
261+
const text = "Hello";
262+
```
263+
264+
### Widget Properties Order
265+
266+
```dart
267+
Widget build(BuildContext context) {
268+
return Container(
269+
// Layout properties first
270+
width: 100,
271+
height: 100,
272+
padding: EdgeInsets.all(16),
273+
margin: EdgeInsets.all(8),
274+
275+
// Decoration properties
276+
decoration: BoxDecoration(
277+
color: Colors.blue,
278+
borderRadius: BorderRadius.circular(8),
279+
),
280+
281+
// Child/children always last
282+
child: Text('Content'),
283+
);
284+
}
285+
```
286+
287+
---
288+
289+
## 🔍 Input Validation
290+
291+
### Assertions for Debug Mode
292+
293+
Use assertions to catch developer errors early:
294+
295+
```dart
296+
Widget build(BuildContext context) {
297+
assert(items.isNotEmpty, 'Items list cannot be empty');
298+
assert(maxCount > 0, 'Max count must be positive');
299+
300+
return ListView.builder(
301+
itemCount: items.length,
302+
itemBuilder: (context, index) => ...,
303+
);
304+
}
305+
```
306+
307+
### Null Safety
308+
309+
Prefer non-nullable types and handle nulls explicitly:
310+
311+
```dart
312+
// ✅ GOOD - Explicit null handling
313+
final displayText = text ?? 'Default';
314+
315+
// ✅ GOOD - Conditional rendering
316+
if (subtitle != null)
317+
Text(subtitle!),
318+
319+
// ❌ BAD - Unsafe null access
320+
Text(subtitle) // Crashes if subtitle is null
321+
```
322+
323+
---
324+
325+
## 📚 Documentation
326+
327+
### Widget Documentation
328+
329+
All public widgets must have doc comments:
330+
331+
```dart
332+
/// A card displaying drink information.
333+
///
334+
/// Shows the drink name, ABV, brewery, and optional rating.
335+
/// Tapping the card navigates to the drink detail screen.
336+
///
337+
/// Example usage:
338+
/// ```dart
339+
/// DrinkCard(
340+
/// drink: myDrink,
341+
/// onTap: () => navigateTo(drinkDetail),
342+
/// )
343+
/// ```
344+
class DrinkCard extends StatelessWidget {
345+
/// Creates a drink card.
346+
const DrinkCard({
347+
required this.drink,
348+
this.onTap,
349+
super.key,
350+
});
351+
352+
/// The drink to display.
353+
final Drink drink;
354+
355+
/// Called when the card is tapped.
356+
final VoidCallback? onTap;
357+
358+
// ...
359+
}
360+
```
361+
362+
---
363+
364+
## ✅ Pre-Commit Checklist
365+
366+
Before committing widget code:
367+
368+
- [ ] Used `SelectableText` for content (not UI labels)
369+
- [ ] Added `Semantics` to interactive elements
370+
- [ ] Used `const` constructors where possible
371+
- [ ] Single quotes for strings
372+
- [ ] Proper overflow handling (`maxLines`, `overflow`)
373+
- [ ] Widget has doc comments
374+
- [ ] Widget tests written and passing
375+
- [ ] Analyzer passes with 0 warnings
376+
- [ ] Accessibility tested (screen reader, large text)
377+
378+
---
379+
380+
## 📖 Related Documentation
381+
382+
- **Accessibility**: [`accessibility.md`](accessibility.md) - Comprehensive accessibility guide
383+
- **Navigation**: [`../navigation.md`](../navigation.md) - Navigation utilities and patterns
384+
- **UI Components**: [`../ui-components.md`](../ui-components.md) - Reusable widget catalog
385+
- **API**: [`api/README.md`](api/README.md) - API integration patterns
386+
387+
---
388+
389+
**Last Updated**: December 2024
390+
**Status**: ✅ Active - All new widgets must follow these standards

0 commit comments

Comments
 (0)