Added Claude Skills
This commit is contained in:
parent
a39e33bc6b
commit
27ebb89052
|
|
@ -1,6 +1,12 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(flutter analyze:*)",
|
||||
"Bash(grep -n \"StreamProvider\\\\|FutureProvider\" /d/tasq/tasq/lib/providers/*.dart)"
|
||||
]
|
||||
},
|
||||
"enableAllProjectMcpServers": true,
|
||||
"enabledMcpjsonServers": [
|
||||
"supabase"
|
||||
],
|
||||
"enableAllProjectMcpServers": true
|
||||
]
|
||||
}
|
||||
|
|
|
|||
516
.claude/skills/animations/SKILL.md
Normal file
516
.claude/skills/animations/SKILL.md
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
---
|
||||
name: custom-plugin-flutter-skill-animations
|
||||
description: Production-grade Flutter animations mastery - Implicit and explicit animations, AnimationController, Hero transitions, physics-based motion, Lottie/Rive integration, 60fps optimization with comprehensive code examples
|
||||
sasmp_version: "1.3.0"
|
||||
bonded_agent: 01-flutter-ui-development
|
||||
bond_type: PRIMARY_BOND
|
||||
---
|
||||
|
||||
# custom-plugin-flutter: Animations Skill
|
||||
|
||||
## Quick Start - Production Animation Pattern
|
||||
|
||||
```dart
|
||||
class AnimatedProductCard extends StatefulWidget {
|
||||
final Product product;
|
||||
final bool isSelected;
|
||||
|
||||
const AnimatedProductCard({
|
||||
required this.product,
|
||||
required this.isSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AnimatedProductCard> createState() => _AnimatedProductCardState();
|
||||
}
|
||||
|
||||
class _AnimatedProductCardState extends State<AnimatedProductCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _scaleAnimation;
|
||||
late final Animation<double> _opacityAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_opacityAnimation = Tween<double>(begin: 1.0, end: 0.8).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTapDown(TapDownDetails details) => _controller.forward();
|
||||
void _onTapUp(TapUpDetails details) => _controller.reverse();
|
||||
void _onTapCancel() => _controller.reverse();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTapDown: _onTapDown,
|
||||
onTapUp: _onTapUp,
|
||||
onTapCancel: _onTapCancel,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Opacity(
|
||||
opacity: _opacityAnimation.value,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: widget.isSelected ? Colors.blue : Colors.grey,
|
||||
width: widget.isSelected ? 2 : 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: ProductContent(product: widget.product),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 1. Implicit Animations
|
||||
|
||||
### Built-in Animated Widgets
|
||||
|
||||
```dart
|
||||
// AnimatedContainer - Animate multiple properties
|
||||
AnimatedContainer(
|
||||
duration: Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
width: isExpanded ? 300 : 100,
|
||||
height: isExpanded ? 200 : 100,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? Colors.blue : Colors.grey,
|
||||
borderRadius: BorderRadius.circular(isExpanded ? 16 : 8),
|
||||
boxShadow: isActive ? [BoxShadow(blurRadius: 10)] : [],
|
||||
),
|
||||
child: content,
|
||||
)
|
||||
|
||||
// AnimatedOpacity
|
||||
AnimatedOpacity(
|
||||
opacity: isVisible ? 1.0 : 0.0,
|
||||
duration: Duration(milliseconds: 200),
|
||||
child: content,
|
||||
)
|
||||
|
||||
// AnimatedPositioned (inside Stack)
|
||||
AnimatedPositioned(
|
||||
duration: Duration(milliseconds: 300),
|
||||
left: isLeft ? 0 : 100,
|
||||
top: isTop ? 0 : 100,
|
||||
child: widget,
|
||||
)
|
||||
|
||||
// AnimatedSwitcher - Cross-fade between widgets
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 300),
|
||||
transitionBuilder: (child, animation) {
|
||||
return FadeTransition(opacity: animation, child: child);
|
||||
},
|
||||
child: Text(
|
||||
currentText,
|
||||
key: ValueKey(currentText), // Important!
|
||||
),
|
||||
)
|
||||
|
||||
// TweenAnimationBuilder - Custom implicit animation
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: progress),
|
||||
duration: Duration(milliseconds: 500),
|
||||
builder: (context, value, child) {
|
||||
return CircularProgressIndicator(value: value);
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## 2. Explicit Animations
|
||||
|
||||
### AnimationController Pattern
|
||||
|
||||
```dart
|
||||
class ExplicitAnimationWidget extends StatefulWidget {
|
||||
@override
|
||||
State<ExplicitAnimationWidget> createState() => _ExplicitAnimationWidgetState();
|
||||
}
|
||||
|
||||
class _ExplicitAnimationWidgetState extends State<ExplicitAnimationWidget>
|
||||
with TickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<Offset> _slideAnimation;
|
||||
late final Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_slideAnimation = Tween<Offset>(
|
||||
begin: Offset(0, 1),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(0.0, 0.6, curve: Curves.easeOut),
|
||||
));
|
||||
|
||||
_fadeAnimation = Tween<double>(
|
||||
begin: 0,
|
||||
end: 1,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(0.3, 1.0, curve: Curves.easeIn),
|
||||
));
|
||||
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Content(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Animation Transitions
|
||||
|
||||
```dart
|
||||
// Built-in transitions
|
||||
FadeTransition(opacity: animation, child: widget)
|
||||
SlideTransition(position: offsetAnimation, child: widget)
|
||||
ScaleTransition(scale: animation, child: widget)
|
||||
RotationTransition(turns: animation, child: widget)
|
||||
SizeTransition(sizeFactor: animation, child: widget)
|
||||
|
||||
// AnimatedBuilder - Custom rendering
|
||||
AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Transform(
|
||||
transform: Matrix4.identity()
|
||||
..setEntry(3, 2, 0.001)
|
||||
..rotateY(_controller.value * pi),
|
||||
alignment: Alignment.center,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Card(),
|
||||
)
|
||||
```
|
||||
|
||||
## 3. Hero Animations
|
||||
|
||||
```dart
|
||||
// Source screen
|
||||
Hero(
|
||||
tag: 'product-${product.id}',
|
||||
child: Image.network(product.imageUrl),
|
||||
)
|
||||
|
||||
// Destination screen
|
||||
Hero(
|
||||
tag: 'product-${product.id}',
|
||||
child: Image.network(product.imageUrl),
|
||||
)
|
||||
|
||||
// Custom Hero flight
|
||||
Hero(
|
||||
tag: 'avatar',
|
||||
flightShuttleBuilder: (
|
||||
flightContext,
|
||||
animation,
|
||||
flightDirection,
|
||||
fromHeroContext,
|
||||
toHeroContext,
|
||||
) {
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, _) {
|
||||
return CircleAvatar(
|
||||
radius: lerpDouble(40, 60, animation.value),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: CircleAvatar(),
|
||||
)
|
||||
```
|
||||
|
||||
## 4. Staggered Animations
|
||||
|
||||
```dart
|
||||
class StaggeredList extends StatefulWidget {
|
||||
@override
|
||||
State<StaggeredList> createState() => _StaggeredListState();
|
||||
}
|
||||
|
||||
class _StaggeredListState extends State<StaggeredList>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
final List<Animation<Offset>> _animations = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: Duration(milliseconds: 1000),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// Create staggered animations
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final start = i * 0.1;
|
||||
final end = start + 0.4;
|
||||
_animations.add(
|
||||
Tween<Offset>(begin: Offset(1, 0), end: Offset.zero).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(start, end, curve: Curves.easeOut),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: List.generate(5, (index) {
|
||||
return SlideTransition(
|
||||
position: _animations[index],
|
||||
child: ListTile(title: Text('Item $index')),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Physics-Based Animations
|
||||
|
||||
```dart
|
||||
// Spring simulation
|
||||
class SpringAnimation extends StatefulWidget {
|
||||
@override
|
||||
State<SpringAnimation> createState() => _SpringAnimationState();
|
||||
}
|
||||
|
||||
class _SpringAnimationState extends State<SpringAnimation>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
final SpringDescription spring = SpringDescription(
|
||||
mass: 1,
|
||||
stiffness: 100,
|
||||
damping: 10,
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(vsync: this);
|
||||
|
||||
final simulation = SpringSimulation(spring, 0, 1, 0);
|
||||
_controller.animateWith(simulation);
|
||||
|
||||
_animation = _controller.drive(Tween(begin: 0.0, end: 100.0));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(0, _animation.value),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Ball(),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Lottie & Rive Integration
|
||||
|
||||
```dart
|
||||
// Lottie animation
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
Lottie.asset(
|
||||
'assets/loading.json',
|
||||
width: 200,
|
||||
height: 200,
|
||||
fit: BoxFit.contain,
|
||||
repeat: true,
|
||||
animate: true,
|
||||
onLoaded: (composition) {
|
||||
_controller.duration = composition.duration;
|
||||
},
|
||||
)
|
||||
|
||||
// Rive animation
|
||||
import 'package:rive/rive.dart';
|
||||
|
||||
RiveAnimation.asset(
|
||||
'assets/animation.riv',
|
||||
fit: BoxFit.cover,
|
||||
stateMachines: ['StateMachine1'],
|
||||
onInit: (artboard) {
|
||||
final controller = StateMachineController.fromArtboard(
|
||||
artboard,
|
||||
'StateMachine1',
|
||||
);
|
||||
artboard.addController(controller!);
|
||||
_trigger = controller.findInput<bool>('trigger') as SMITrigger;
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## 7. Page Transitions
|
||||
|
||||
```dart
|
||||
// Custom page route
|
||||
class FadePageRoute<T> extends PageRouteBuilder<T> {
|
||||
final Widget page;
|
||||
|
||||
FadePageRoute({required this.page})
|
||||
: super(
|
||||
pageBuilder: (context, animation, secondaryAnimation) => page,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
return FadeTransition(opacity: animation, child: child);
|
||||
},
|
||||
transitionDuration: Duration(milliseconds: 300),
|
||||
);
|
||||
}
|
||||
|
||||
// Slide + fade combined
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
final offsetAnimation = Tween<Offset>(
|
||||
begin: Offset(1.0, 0.0),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
return SlideTransition(
|
||||
position: offsetAnimation,
|
||||
child: FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Performance Optimization
|
||||
|
||||
```dart
|
||||
// Use const where possible
|
||||
const AnimatedContainer(...)
|
||||
|
||||
// RepaintBoundary for expensive animations
|
||||
RepaintBoundary(
|
||||
child: AnimatedWidget(),
|
||||
)
|
||||
|
||||
// AnimatedBuilder isolates rebuilds
|
||||
AnimatedBuilder(
|
||||
animation: _controller,
|
||||
child: ExpensiveWidget(), // Not rebuilt
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _controller.value,
|
||||
child: child, // Reused
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
// Avoid layout-triggering animations
|
||||
// Good: Transform, Opacity
|
||||
// Avoid: width/height in tight loops
|
||||
```
|
||||
|
||||
## Troubleshooting Guide
|
||||
|
||||
**Issue: Animation jank (dropped frames)**
|
||||
```
|
||||
1. Use RepaintBoundary to isolate
|
||||
2. Profile with DevTools Performance
|
||||
3. Avoid layout changes during animation
|
||||
4. Use AnimatedBuilder, not setState
|
||||
5. Check for heavy build methods
|
||||
```
|
||||
|
||||
**Issue: Animation doesn't start**
|
||||
```
|
||||
1. Verify AnimationController.forward() called
|
||||
2. Check vsync: this (TickerProviderStateMixin)
|
||||
3. Verify widget is mounted before animating
|
||||
4. Check duration is set
|
||||
```
|
||||
|
||||
**Issue: Animation leaks memory**
|
||||
```
|
||||
1. Dispose AnimationController in dispose()
|
||||
2. Cancel any listeners
|
||||
3. Use mounted check before setState
|
||||
```
|
||||
|
||||
## Animation Selection Guide
|
||||
|
||||
| Need | Solution |
|
||||
|------|----------|
|
||||
| Simple property change | AnimatedContainer |
|
||||
| Cross-fade widgets | AnimatedSwitcher |
|
||||
| Custom timing control | AnimationController |
|
||||
| Shared element | Hero |
|
||||
| List items appearing | Staggered animations |
|
||||
| Natural motion | Physics simulation |
|
||||
| Complex vector | Lottie/Rive |
|
||||
|
||||
---
|
||||
|
||||
**Create fluid, 60fps animations in Flutter.**
|
||||
41
.claude/skills/animations/assets/config.yaml
Normal file
41
.claude/skills/animations/assets/config.yaml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# animations Configuration
|
||||
# Category: general
|
||||
# Generated: 2025-12-30
|
||||
|
||||
skill:
|
||||
name: animations
|
||||
version: "1.0.0"
|
||||
category: general
|
||||
|
||||
settings:
|
||||
# Default settings for animations
|
||||
enabled: true
|
||||
log_level: info
|
||||
|
||||
# Category-specific defaults
|
||||
validation:
|
||||
strict_mode: false
|
||||
auto_fix: false
|
||||
|
||||
output:
|
||||
format: markdown
|
||||
include_examples: true
|
||||
|
||||
# Environment-specific overrides
|
||||
environments:
|
||||
development:
|
||||
log_level: debug
|
||||
validation:
|
||||
strict_mode: false
|
||||
|
||||
production:
|
||||
log_level: warn
|
||||
validation:
|
||||
strict_mode: true
|
||||
|
||||
# Integration settings
|
||||
integrations:
|
||||
# Enable/disable integrations
|
||||
git: true
|
||||
linter: true
|
||||
formatter: true
|
||||
60
.claude/skills/animations/assets/schema.json
Normal file
60
.claude/skills/animations/assets/schema.json
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "animations Configuration Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"skill": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+$"
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"api",
|
||||
"testing",
|
||||
"devops",
|
||||
"security",
|
||||
"database",
|
||||
"frontend",
|
||||
"algorithms",
|
||||
"machine-learning",
|
||||
"cloud",
|
||||
"containers",
|
||||
"general"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"version"
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"log_level": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"debug",
|
||||
"info",
|
||||
"warn",
|
||||
"error"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"skill"
|
||||
]
|
||||
}
|
||||
95
.claude/skills/animations/references/GUIDE.md
Normal file
95
.claude/skills/animations/references/GUIDE.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# Animations Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide provides comprehensive documentation for the **animations** skill in the custom-plugin-flutter plugin.
|
||||
|
||||
## Category: General
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Familiarity with general concepts
|
||||
- Development environment set up
|
||||
- Plugin installed and configured
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
# Invoke the skill
|
||||
claude "animations - [your task description]"
|
||||
|
||||
# Example
|
||||
claude "animations - analyze the current implementation"
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Key Principles
|
||||
|
||||
1. **Consistency** - Follow established patterns
|
||||
2. **Clarity** - Write readable, maintainable code
|
||||
3. **Quality** - Validate before deployment
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Always validate input data
|
||||
- Handle edge cases explicitly
|
||||
- Document your decisions
|
||||
- Write tests for critical paths
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Task 1: Basic Implementation
|
||||
|
||||
```python
|
||||
# Example implementation pattern
|
||||
def implement_animations(input_data):
|
||||
"""
|
||||
Implement animations functionality.
|
||||
|
||||
Args:
|
||||
input_data: Input to process
|
||||
|
||||
Returns:
|
||||
Processed result
|
||||
"""
|
||||
# Validate input
|
||||
if not input_data:
|
||||
raise ValueError("Input required")
|
||||
|
||||
# Process
|
||||
result = process(input_data)
|
||||
|
||||
# Return
|
||||
return result
|
||||
```
|
||||
|
||||
### Task 2: Advanced Usage
|
||||
|
||||
For advanced scenarios, consider:
|
||||
|
||||
- Configuration customization via `assets/config.yaml`
|
||||
- Validation using `scripts/validate.py`
|
||||
- Integration with other skills
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
| Issue | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| Skill not found | Not installed | Run plugin sync |
|
||||
| Validation fails | Invalid config | Check config.yaml |
|
||||
| Unexpected output | Missing context | Provide more details |
|
||||
|
||||
## Related Resources
|
||||
|
||||
- SKILL.md - Skill specification
|
||||
- config.yaml - Configuration options
|
||||
- validate.py - Validation script
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2025-12-30*
|
||||
87
.claude/skills/animations/references/PATTERNS.md
Normal file
87
.claude/skills/animations/references/PATTERNS.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Animations Patterns
|
||||
|
||||
## Design Patterns
|
||||
|
||||
### Pattern 1: Input Validation
|
||||
|
||||
Always validate input before processing:
|
||||
|
||||
```python
|
||||
def validate_input(data):
|
||||
if data is None:
|
||||
raise ValueError("Data cannot be None")
|
||||
if not isinstance(data, dict):
|
||||
raise TypeError("Data must be a dictionary")
|
||||
return True
|
||||
```
|
||||
|
||||
### Pattern 2: Error Handling
|
||||
|
||||
Use consistent error handling:
|
||||
|
||||
```python
|
||||
try:
|
||||
result = risky_operation()
|
||||
except SpecificError as e:
|
||||
logger.error(f"Operation failed: {e}")
|
||||
handle_error(e)
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error")
|
||||
raise
|
||||
```
|
||||
|
||||
### Pattern 3: Configuration Loading
|
||||
|
||||
Load and validate configuration:
|
||||
|
||||
```python
|
||||
import yaml
|
||||
|
||||
def load_config(config_path):
|
||||
with open(config_path) as f:
|
||||
config = yaml.safe_load(f)
|
||||
validate_config(config)
|
||||
return config
|
||||
```
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### ❌ Don't: Swallow Exceptions
|
||||
|
||||
```python
|
||||
# BAD
|
||||
try:
|
||||
do_something()
|
||||
except:
|
||||
pass
|
||||
```
|
||||
|
||||
### ✅ Do: Handle Explicitly
|
||||
|
||||
```python
|
||||
# GOOD
|
||||
try:
|
||||
do_something()
|
||||
except SpecificError as e:
|
||||
logger.warning(f"Expected error: {e}")
|
||||
return default_value
|
||||
```
|
||||
|
||||
## Category-Specific Patterns: General
|
||||
|
||||
### Recommended Approach
|
||||
|
||||
1. Start with the simplest implementation
|
||||
2. Add complexity only when needed
|
||||
3. Test each addition
|
||||
4. Document decisions
|
||||
|
||||
### Common Integration Points
|
||||
|
||||
- Configuration: `assets/config.yaml`
|
||||
- Validation: `scripts/validate.py`
|
||||
- Documentation: `references/GUIDE.md`
|
||||
|
||||
---
|
||||
|
||||
*Pattern library for animations skill*
|
||||
131
.claude/skills/animations/scripts/validate.py
Normal file
131
.claude/skills/animations/scripts/validate.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Validation script for animations skill.
|
||||
Category: general
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def validate_config(config_path: str) -> dict:
|
||||
"""
|
||||
Validate skill configuration file.
|
||||
|
||||
Args:
|
||||
config_path: Path to config.yaml
|
||||
|
||||
Returns:
|
||||
dict: Validation result with 'valid' and 'errors' keys
|
||||
"""
|
||||
errors = []
|
||||
|
||||
if not os.path.exists(config_path):
|
||||
return {"valid": False, "errors": ["Config file not found"]}
|
||||
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
config = yaml.safe_load(f)
|
||||
except yaml.YAMLError as e:
|
||||
return {"valid": False, "errors": [f"YAML parse error: {e}"]}
|
||||
|
||||
# Validate required fields
|
||||
if 'skill' not in config:
|
||||
errors.append("Missing 'skill' section")
|
||||
else:
|
||||
if 'name' not in config['skill']:
|
||||
errors.append("Missing skill.name")
|
||||
if 'version' not in config['skill']:
|
||||
errors.append("Missing skill.version")
|
||||
|
||||
# Validate settings
|
||||
if 'settings' in config:
|
||||
settings = config['settings']
|
||||
if 'log_level' in settings:
|
||||
valid_levels = ['debug', 'info', 'warn', 'error']
|
||||
if settings['log_level'] not in valid_levels:
|
||||
errors.append(f"Invalid log_level: {settings['log_level']}")
|
||||
|
||||
return {
|
||||
"valid": len(errors) == 0,
|
||||
"errors": errors,
|
||||
"config": config if not errors else None
|
||||
}
|
||||
|
||||
|
||||
def validate_skill_structure(skill_path: str) -> dict:
|
||||
"""
|
||||
Validate skill directory structure.
|
||||
|
||||
Args:
|
||||
skill_path: Path to skill directory
|
||||
|
||||
Returns:
|
||||
dict: Structure validation result
|
||||
"""
|
||||
required_dirs = ['assets', 'scripts', 'references']
|
||||
required_files = ['SKILL.md']
|
||||
|
||||
errors = []
|
||||
|
||||
# Check required files
|
||||
for file in required_files:
|
||||
if not os.path.exists(os.path.join(skill_path, file)):
|
||||
errors.append(f"Missing required file: {file}")
|
||||
|
||||
# Check required directories
|
||||
for dir in required_dirs:
|
||||
dir_path = os.path.join(skill_path, dir)
|
||||
if not os.path.isdir(dir_path):
|
||||
errors.append(f"Missing required directory: {dir}/")
|
||||
else:
|
||||
# Check for real content (not just .gitkeep)
|
||||
files = [f for f in os.listdir(dir_path) if f != '.gitkeep']
|
||||
if not files:
|
||||
errors.append(f"Directory {dir}/ has no real content")
|
||||
|
||||
return {
|
||||
"valid": len(errors) == 0,
|
||||
"errors": errors,
|
||||
"skill_name": os.path.basename(skill_path)
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Main validation entry point."""
|
||||
skill_path = Path(__file__).parent.parent
|
||||
|
||||
print(f"Validating animations skill...")
|
||||
print(f"Path: {skill_path}")
|
||||
|
||||
# Validate structure
|
||||
structure_result = validate_skill_structure(str(skill_path))
|
||||
print(f"\nStructure validation: {'PASS' if structure_result['valid'] else 'FAIL'}")
|
||||
if structure_result['errors']:
|
||||
for error in structure_result['errors']:
|
||||
print(f" - {error}")
|
||||
|
||||
# Validate config
|
||||
config_path = skill_path / 'assets' / 'config.yaml'
|
||||
if config_path.exists():
|
||||
config_result = validate_config(str(config_path))
|
||||
print(f"\nConfig validation: {'PASS' if config_result['valid'] else 'FAIL'}")
|
||||
if config_result['errors']:
|
||||
for error in config_result['errors']:
|
||||
print(f" - {error}")
|
||||
else:
|
||||
print("\nConfig validation: SKIPPED (no config.yaml)")
|
||||
|
||||
# Summary
|
||||
all_valid = structure_result['valid']
|
||||
print(f"\n==================================================")
|
||||
print(f"Overall: {'VALID' if all_valid else 'INVALID'}")
|
||||
|
||||
return 0 if all_valid else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
39
.claude/skills/deepsearch/SKILL.md
Normal file
39
.claude/skills/deepsearch/SKILL.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
name: deepsearch
|
||||
description: Thorough codebase search
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# Deep Search Mode
|
||||
|
||||
[DEEPSEARCH MODE ACTIVATED]
|
||||
|
||||
## Objective
|
||||
|
||||
Perform thorough search of the codebase for the specified query, pattern, or concept.
|
||||
|
||||
## Search Strategy
|
||||
|
||||
1. **Broad Search**
|
||||
- Search for exact matches
|
||||
- Search for related terms and variations
|
||||
- Check common locations (components, utils, services, hooks)
|
||||
|
||||
2. **Deep Dive**
|
||||
- Read files with matches
|
||||
- Check imports/exports to find connections
|
||||
- Follow the trail (what imports this? what does this import?)
|
||||
|
||||
3. **Synthesize**
|
||||
- Map out where the concept is used
|
||||
- Identify the main implementation
|
||||
- Note related functionality
|
||||
|
||||
## Output Format
|
||||
|
||||
- **Primary Locations** (main implementations)
|
||||
- **Related Files** (dependencies, consumers)
|
||||
- **Usage Patterns** (how it's used across the codebase)
|
||||
- **Key Insights** (patterns, conventions, gotchas)
|
||||
|
||||
Focus on being comprehensive but concise. Cite file paths and line numbers.
|
||||
316
.claude/skills/flutter-development/SKILL.md
Normal file
316
.claude/skills/flutter-development/SKILL.md
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
---
|
||||
name: flutter-development
|
||||
description: Build beautiful cross-platform mobile apps with Flutter and Dart. Covers widgets, state management with Provider/BLoC, navigation, API integration, and material design.
|
||||
---
|
||||
|
||||
# Flutter Development
|
||||
|
||||
## Overview
|
||||
|
||||
Create high-performance, visually stunning mobile applications using Flutter with Dart language. Master widget composition, state management patterns, navigation, and API integration.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Building iOS and Android apps with native performance
|
||||
- Designing custom UIs with Flutter's widget system
|
||||
- Implementing complex animations and visual effects
|
||||
- Rapid app development with hot reload
|
||||
- Creating consistent UX across platforms
|
||||
|
||||
## Instructions
|
||||
|
||||
### 1. **Project Structure & Navigation**
|
||||
|
||||
```dart
|
||||
// pubspec.yaml
|
||||
name: my_flutter_app
|
||||
version: 1.0.0
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
provider: ^6.0.0
|
||||
http: ^1.1.0
|
||||
go_router: ^12.0.0
|
||||
|
||||
// main.dart with GoRouter navigation
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
title: 'Flutter App',
|
||||
theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
|
||||
routerConfig: _router,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final GoRouter _router = GoRouter(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => const HomeScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'details/:id',
|
||||
builder: (context, state) => DetailsScreen(
|
||||
itemId: state.pathParameters['id']!
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile',
|
||||
builder: (context, state) => const ProfileScreen(),
|
||||
),
|
||||
],
|
||||
);
|
||||
```
|
||||
|
||||
### 2. **State Management with Provider**
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
|
||||
class User {
|
||||
final String id;
|
||||
final String name;
|
||||
final String email;
|
||||
|
||||
User({required this.id, required this.name, required this.email});
|
||||
|
||||
factory User.fromJson(Map<String, dynamic> json) {
|
||||
return User(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
email: json['email'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UserProvider extends ChangeNotifier {
|
||||
User? _user;
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
|
||||
User? get user => _user;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
|
||||
Future<void> fetchUser(String userId) async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse('https://api.example.com/users/$userId'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
_user = User.fromJson(jsonDecode(response.body));
|
||||
} else {
|
||||
_error = 'Failed to fetch user';
|
||||
}
|
||||
} catch (e) {
|
||||
_error = 'Error: ${e.toString()}';
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void logout() {
|
||||
_user = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class ItemsProvider extends ChangeNotifier {
|
||||
List<Map<String, dynamic>> _items = [];
|
||||
|
||||
List<Map<String, dynamic>> get items => _items;
|
||||
|
||||
Future<void> fetchItems() async {
|
||||
try {
|
||||
final response = await http.get(
|
||||
Uri.parse('https://api.example.com/items'),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
_items = List<Map<String, dynamic>>.from(
|
||||
jsonDecode(response.body) as List
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching items: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **Screens with Provider Integration**
|
||||
|
||||
```dart
|
||||
class HomeScreen extends StatefulWidget {
|
||||
const HomeScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.microtask(() {
|
||||
Provider.of<ItemsProvider>(context, listen: false).fetchItems();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Home Feed')),
|
||||
body: Consumer<ItemsProvider>(
|
||||
builder: (context, itemsProvider, child) {
|
||||
if (itemsProvider.items.isEmpty) {
|
||||
return const Center(child: Text('No items found'));
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: itemsProvider.items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = itemsProvider.items[index];
|
||||
return ItemCard(item: item);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ItemCard extends StatelessWidget {
|
||||
final Map<String, dynamic> item;
|
||||
|
||||
const ItemCard({required this.item, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: ListTile(
|
||||
title: Text(item['title'] ?? 'Untitled'),
|
||||
subtitle: Text(item['description'] ?? ''),
|
||||
trailing: const Icon(Icons.arrow_forward),
|
||||
onTap: () => context.go('/details/${item['id']}'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DetailsScreen extends StatelessWidget {
|
||||
final String itemId;
|
||||
|
||||
const DetailsScreen({required this.itemId, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Details')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('Item ID: $itemId', style: const TextStyle(fontSize: 18)),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: const Text('Go Back'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileScreen extends StatelessWidget {
|
||||
const ProfileScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Profile')),
|
||||
body: Consumer<UserProvider>(
|
||||
builder: (context, userProvider, child) {
|
||||
if (userProvider.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (userProvider.error != null) {
|
||||
return Center(child: Text('Error: ${userProvider.error}'));
|
||||
}
|
||||
final user = userProvider.user;
|
||||
if (user == null) {
|
||||
return const Center(child: Text('No user data'));
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Name: ${user.name}', style: const TextStyle(fontSize: 18)),
|
||||
Text('Email: ${user.email}', style: const TextStyle(fontSize: 16)),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => userProvider.logout(),
|
||||
child: const Text('Logout'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO
|
||||
- Use widgets for every UI element
|
||||
- Implement proper state management
|
||||
- Use const constructors where possible
|
||||
- Dispose resources in state lifecycle
|
||||
- Test on multiple device sizes
|
||||
- Use meaningful widget names
|
||||
- Implement error handling
|
||||
- Use responsive design patterns
|
||||
- Test on both iOS and Android
|
||||
- Document custom widgets
|
||||
|
||||
### ❌ DON'T
|
||||
- Build entire screens in build() method
|
||||
- Use setState for complex state logic
|
||||
- Make network calls in build()
|
||||
- Ignore platform differences
|
||||
- Create overly nested widget trees
|
||||
- Hardcode strings
|
||||
- Ignore performance warnings
|
||||
- Skip testing
|
||||
- Forget to handle edge cases
|
||||
- Deploy without thorough testing
|
||||
14
.claude/skills/flutter-development/metadata.json
Normal file
14
.claude/skills/flutter-development/metadata.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"id": "aj-geddes-useful-ai-prompts-skills-flutter-development-skill-md",
|
||||
"name": "flutter-development",
|
||||
"author": "aj-geddes",
|
||||
"authorAvatar": "https://avatars.githubusercontent.com/u/211219442?v=4",
|
||||
"description": "Build beautiful cross-platform mobile apps with Flutter and Dart. Covers widgets, state management with Provider/BLoC, navigation, API integration, and material design.",
|
||||
"githubUrl": "https://github.com/aj-geddes/useful-ai-prompts/tree/main/skills/flutter-development",
|
||||
"stars": 12,
|
||||
"forks": 0,
|
||||
"updatedAt": 1764499235,
|
||||
"hasMarketplace": false,
|
||||
"path": "SKILL.md",
|
||||
"branch": "main"
|
||||
}
|
||||
614
.claude/skills/flutter-ui/SKILL.md
Normal file
614
.claude/skills/flutter-ui/SKILL.md
Normal file
|
|
@ -0,0 +1,614 @@
|
|||
---
|
||||
name: custom-plugin-flutter-skill-ui
|
||||
description: 1700+ lines of Flutter UI mastery - widgets, layouts, Material Design, animations, responsive design with production-ready code examples and enterprise patterns.
|
||||
sasmp_version: "1.3.0"
|
||||
bonded_agent: 01-flutter-ui-development
|
||||
bond_type: PRIMARY_BOND
|
||||
---
|
||||
|
||||
# custom-plugin-flutter: UI Development Skill
|
||||
|
||||
## Quick Start - Production UI Pattern
|
||||
|
||||
```dart
|
||||
class ResponsiveProductScreen extends ConsumerWidget {
|
||||
const ResponsiveProductScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||
final productState = ref.watch(productProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Products'),
|
||||
elevation: 0,
|
||||
),
|
||||
body: productState.when(
|
||||
loading: () => const LoadingWidget(),
|
||||
data: (products) => isMobile
|
||||
? _MobileProductList(products: products)
|
||||
: _DesktopProductGrid(products: products),
|
||||
error: (error, st) => ErrorWidget(error: error.toString()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MobileProductList extends StatelessWidget {
|
||||
final List<Product> products;
|
||||
const _MobileProductList({required this.products});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
itemCount: products.length,
|
||||
itemBuilder: (context, index) => ProductCard(product: products[index]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DesktopProductGrid extends StatelessWidget {
|
||||
final List<Product> products;
|
||||
const _DesktopProductGrid({required this.products});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 0.8,
|
||||
),
|
||||
itemCount: products.length,
|
||||
itemBuilder: (context, index) => ProductCard(product: products[index]),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 1. Widget System Mastery
|
||||
|
||||
### Understanding the Widget Tree
|
||||
|
||||
**Stateless Widgets** - Pure, immutable widgets:
|
||||
```dart
|
||||
class PureWidget extends StatelessWidget {
|
||||
final String title;
|
||||
const PureWidget({required this.title});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Text(title);
|
||||
}
|
||||
```
|
||||
|
||||
**Stateful Widgets** - Managing internal state:
|
||||
```dart
|
||||
class CounterWidget extends StatefulWidget {
|
||||
const CounterWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CounterWidget> createState() => _CounterWidgetState();
|
||||
}
|
||||
|
||||
class _CounterWidgetState extends State<CounterWidget> {
|
||||
int _count = 0;
|
||||
|
||||
void _increment() {
|
||||
setState(() => _count++);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialize resources
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Clean up resources
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text('Count: $_count'),
|
||||
ElevatedButton(
|
||||
onPressed: _increment,
|
||||
child: const Text('Increment'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Const Constructors** - Preventing unnecessary rebuilds:
|
||||
```dart
|
||||
// ✅ Good - Const constructor
|
||||
const SizedBox(height: 16, child: Text('Hello'))
|
||||
|
||||
// ❌ Bad - Non-const, rebuilds every time
|
||||
SizedBox(height: 16, child: Text('Hello'))
|
||||
|
||||
// ✅ Make all widgets const when possible
|
||||
class MyWidget extends StatelessWidget {
|
||||
const MyWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => const Placeholder();
|
||||
}
|
||||
```
|
||||
|
||||
**Inherited Widgets** - Efficient state propagation:
|
||||
```dart
|
||||
class ThemeProvider extends InheritedWidget {
|
||||
final ThemeData theme;
|
||||
|
||||
const ThemeProvider({
|
||||
required this.theme,
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static ThemeProvider of(BuildContext context) {
|
||||
final result = context.dependOnInheritedWidgetOfExactType<ThemeProvider>();
|
||||
assert(result != null, 'No ThemeProvider found in context');
|
||||
return result!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ThemeProvider oldWidget) => theme != oldWidget.theme;
|
||||
}
|
||||
|
||||
// Usage
|
||||
class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ThemeProvider(
|
||||
theme: ThemeData.light(),
|
||||
child: MaterialApp(home: MyScreen()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = ThemeProvider.of(context).theme;
|
||||
return Scaffold(
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Constraint-Based Layout System
|
||||
|
||||
### Understanding Constraints
|
||||
|
||||
**Basic Constraint Flow**:
|
||||
```dart
|
||||
// Parent imposes constraints on children
|
||||
// Child sizes itself based on constraints
|
||||
// Parent positions child based on alignment
|
||||
|
||||
Center( // Imposes tight constraint
|
||||
child: Container(
|
||||
width: 200,
|
||||
height: 100,
|
||||
color: Colors.blue,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
**Layout Widgets**:
|
||||
```dart
|
||||
// Row - horizontal layout
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text('Left'),
|
||||
Text('Middle'),
|
||||
Text('Right'),
|
||||
],
|
||||
)
|
||||
|
||||
// Column - vertical layout
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Container(height: 100, color: Colors.red),
|
||||
Container(height: 100, color: Colors.blue),
|
||||
Container(height: 100, color: Colors.green),
|
||||
],
|
||||
)
|
||||
|
||||
// Flex - flexible layout
|
||||
Flex(
|
||||
direction: Axis.horizontal,
|
||||
children: [
|
||||
Flexible(flex: 2, child: Container(color: Colors.red)),
|
||||
Flexible(flex: 1, child: Container(color: Colors.blue)),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
**Advanced Layouts**:
|
||||
```dart
|
||||
// GridView - grid layout
|
||||
GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemCount: 12,
|
||||
itemBuilder: (context, index) => Container(
|
||||
color: Colors.blue[100 * ((index % 9) + 1)],
|
||||
),
|
||||
)
|
||||
|
||||
// Stack - overlaying widgets
|
||||
Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
Image.asset('background.png'),
|
||||
Positioned(
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
child: FloatingActionButton(onPressed: () {}),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
// CustomMultiChildLayout - manual layout
|
||||
CustomMultiChildLayout(
|
||||
delegate: MyLayoutDelegate(),
|
||||
children: [
|
||||
LayoutId(id: 'title', child: Text('Title')),
|
||||
LayoutId(id: 'body', child: Text('Body')),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## 3. Material Design 3 Implementation
|
||||
|
||||
### Theme Configuration
|
||||
|
||||
```dart
|
||||
class AppTheme {
|
||||
static ThemeData lightTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.blue,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
typography: Typography.material2021(
|
||||
platform: defaultTargetPlatform,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
elevation: 0,
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
cardTheme: CardTheme(
|
||||
elevation: 2,
|
||||
margin: EdgeInsets.all(16),
|
||||
),
|
||||
);
|
||||
|
||||
static ThemeData darkTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Colors.blue,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
MaterialApp(
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
themeMode: ThemeMode.system,
|
||||
)
|
||||
```
|
||||
|
||||
### Material Components
|
||||
|
||||
```dart
|
||||
// Modern AppBar
|
||||
AppBar(
|
||||
title: Text('Title'),
|
||||
elevation: 0,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
)
|
||||
|
||||
// Enhanced Button
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {},
|
||||
icon: Icon(Icons.send),
|
||||
label: Text('Send'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
)
|
||||
|
||||
// Material TextField
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Enter name',
|
||||
hintText: 'John Doe',
|
||||
prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[100],
|
||||
),
|
||||
)
|
||||
|
||||
// Material Form
|
||||
Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
if (value?.isEmpty ?? true) return 'Required';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Submit form
|
||||
}
|
||||
},
|
||||
child: Text('Submit'),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## 4. Animation Framework
|
||||
|
||||
### AnimationController Pattern
|
||||
|
||||
```dart
|
||||
class AnimatedCounterWidget extends StatefulWidget {
|
||||
const AnimatedCounterWidget();
|
||||
|
||||
@override
|
||||
State<AnimatedCounterWidget> createState() => _AnimatedCounterWidgetState();
|
||||
}
|
||||
|
||||
class _AnimatedCounterWidgetState extends State<AnimatedCounterWidget>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
int _count = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
_animation = Tween<double>(begin: 1, end: 0.8).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
);
|
||||
}
|
||||
|
||||
void _increment() {
|
||||
setState(() => _count++);
|
||||
_controller.forward().then((_) => _controller.reverse());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScaleTransition(
|
||||
scale: _animation,
|
||||
child: GestureDetector(
|
||||
onTap: _increment,
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.blue,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'$_count',
|
||||
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Implicit Animations
|
||||
|
||||
```dart
|
||||
// AnimatedContainer - animate properties smoothly
|
||||
class AnimatedBoxWidget extends StatefulWidget {
|
||||
@override
|
||||
State<AnimatedBoxWidget> createState() => _AnimatedBoxWidgetState();
|
||||
}
|
||||
|
||||
class _AnimatedBoxWidgetState extends State<AnimatedBoxWidget> {
|
||||
bool _expanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _expanded = !_expanded),
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 500),
|
||||
width: _expanded ? 300 : 100,
|
||||
height: _expanded ? 300 : 100,
|
||||
decoration: BoxDecoration(
|
||||
color: _expanded ? Colors.blue : Colors.red,
|
||||
borderRadius: BorderRadius.circular(_expanded ? 16 : 8),
|
||||
),
|
||||
child: Center(
|
||||
child: AnimatedOpacity(
|
||||
opacity: _expanded ? 1 : 0,
|
||||
duration: Duration(milliseconds: 500),
|
||||
child: Text('Expanded'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Responsive Design Pattern
|
||||
|
||||
```dart
|
||||
class ResponsiveWidget extends StatelessWidget {
|
||||
final Widget Function(BuildContext) mobileBuilder;
|
||||
final Widget Function(BuildContext) tabletBuilder;
|
||||
final Widget Function(BuildContext) desktopBuilder;
|
||||
|
||||
const ResponsiveWidget({
|
||||
required this.mobileBuilder,
|
||||
required this.tabletBuilder,
|
||||
required this.desktopBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
|
||||
if (width < 600) {
|
||||
return mobileBuilder(context);
|
||||
} else if (width < 1200) {
|
||||
return tabletBuilder(context);
|
||||
} else {
|
||||
return desktopBuilder(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
ResponsiveWidget(
|
||||
mobileBuilder: (context) => MobileLayout(),
|
||||
tabletBuilder: (context) => TabletLayout(),
|
||||
desktopBuilder: (context) => DesktopLayout(),
|
||||
)
|
||||
```
|
||||
|
||||
## 6. Accessibility Best Practices
|
||||
|
||||
```dart
|
||||
class AccessibleWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: 'Submit form',
|
||||
enabled: true,
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
constraints: BoxConstraints(minHeight: 48, minWidth: 48),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Submit',
|
||||
style: TextStyle(fontSize: 16), // Respect system font scaling
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Performance Optimization Tips
|
||||
|
||||
- ✅ Use `const` constructors everywhere possible
|
||||
- ✅ Extract widgets to separate classes to limit rebuilds
|
||||
- ✅ Use `RepaintBoundary` for expensive rendering
|
||||
- ✅ Use `ListView.builder` instead of `ListView`
|
||||
- ✅ Cache images with `CachedNetworkImage`
|
||||
- ✅ Profile with DevTools regularly
|
||||
- ✅ Use `LayoutBuilder` for responsive design
|
||||
- ✅ Prefer `SingleChildScrollView` over custom scrolling
|
||||
|
||||
## 8. Custom Widget Template
|
||||
|
||||
```dart
|
||||
class CustomButton extends StatelessWidget {
|
||||
final VoidCallback onPressed;
|
||||
final String label;
|
||||
final ButtonSize size;
|
||||
|
||||
const CustomButton({
|
||||
required this.onPressed,
|
||||
required this.label,
|
||||
this.size = ButtonSize.medium,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: InkWell(
|
||||
onTap: onPressed,
|
||||
child: Container(
|
||||
padding: _paddingForSize(size),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
EdgeInsets _paddingForSize(ButtonSize size) {
|
||||
switch (size) {
|
||||
case ButtonSize.small:
|
||||
return EdgeInsets.symmetric(horizontal: 12, vertical: 8);
|
||||
case ButtonSize.medium:
|
||||
return EdgeInsets.symmetric(horizontal: 16, vertical: 12);
|
||||
case ButtonSize.large:
|
||||
return EdgeInsets.symmetric(horizontal: 24, vertical: 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ButtonSize { small, medium, large }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Master Flutter UI development with this comprehensive skill reference.**
|
||||
20
.claude/skills/flutter-ui/assets/ui_config.yaml
Normal file
20
.claude/skills/flutter-ui/assets/ui_config.yaml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
widgets:
|
||||
stateless:
|
||||
- Container
|
||||
- Text
|
||||
- Icon
|
||||
- Image
|
||||
stateful:
|
||||
- TextField
|
||||
- ListView
|
||||
- AnimatedWidget
|
||||
layouts:
|
||||
- Column
|
||||
- Row
|
||||
- Stack
|
||||
- GridView
|
||||
- CustomScrollView
|
||||
theming:
|
||||
- material_3
|
||||
- cupertino
|
||||
- custom_theme
|
||||
12
.claude/skills/flutter-ui/references/UI_GUIDE.md
Normal file
12
.claude/skills/flutter-ui/references/UI_GUIDE.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Flutter UI Guide
|
||||
## Widget System
|
||||
- Everything is a widget
|
||||
- Composition over inheritance
|
||||
- StatelessWidget for static UI
|
||||
- StatefulWidget for dynamic UI
|
||||
|
||||
## Layout Widgets
|
||||
- Column/Row: Linear layout
|
||||
- Stack: Overlapping
|
||||
- ListView: Scrollable lists
|
||||
- GridView: Grid layouts
|
||||
5
.claude/skills/flutter-ui/scripts/widget_analyzer.py
Normal file
5
.claude/skills/flutter-ui/scripts/widget_analyzer.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Flutter widget analyzer."""
|
||||
import json
|
||||
def analyze(): return {"categories": ["stateless", "stateful", "inherited", "render"], "layouts": ["flex", "stack", "custom"]}
|
||||
if __name__ == "__main__": print(json.dumps(analyze(), indent=2))
|
||||
42
.claude/skills/frontend-design/SKILL.md
Normal file
42
.claude/skills/frontend-design/SKILL.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
327
.claude/skills/material-thinking/SKILL.md
Normal file
327
.claude/skills/material-thinking/SKILL.md
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
---
|
||||
name: material-thinking
|
||||
description: Comprehensive Material Design 3 (M3) and M3 Expressive guidance for building modern, accessible, and engaging user interfaces. Use when designing or implementing Material Design interfaces, reviewing component designs for M3 compliance, generating design tokens (color schemes, typography, shapes), applying M3 Expressive motion and interactions, or migrating existing UIs to Material 3. Covers all 38 M3 components, foundations (accessibility, layout, interaction), styles (color, typography, elevation, shape, icons, motion), and M3 Expressive tactics for more engaging experiences.
|
||||
---
|
||||
|
||||
# Material Thinking
|
||||
|
||||
Apply Material Design 3 and M3 Expressive principles to create accessible, consistent, and engaging user interfaces.
|
||||
|
||||
## Overview
|
||||
|
||||
This skill provides comprehensive guidance for implementing Material Design 3 (M3) and M3 Expressive across all platforms. Material Design 3 is Google's open-source design system that provides UX guidance, reusable components, and design tools for creating beautiful, accessible interfaces.
|
||||
|
||||
**Key capabilities:**
|
||||
- Design new products with M3 principles
|
||||
- Review existing designs for M3 compliance
|
||||
- Generate design tokens (colors, typography, shapes)
|
||||
- Apply M3 Expressive for engaging, emotionally resonant UIs
|
||||
- Select appropriate components for specific use cases
|
||||
- Ensure accessibility and responsive design
|
||||
|
||||
## Core Resources
|
||||
|
||||
This skill includes four comprehensive reference documents. Read these as needed during your work:
|
||||
|
||||
### 1. Foundations (`references/foundations.md`)
|
||||
Read when working with:
|
||||
- Accessibility requirements
|
||||
- Layout and responsive design (window size classes, canonical layouts)
|
||||
- Interaction patterns (states, gestures, selection)
|
||||
- Content design and UX writing
|
||||
- Design tokens and adaptive design
|
||||
|
||||
### 2. Styles (`references/styles.md`)
|
||||
Read when working with:
|
||||
- Color systems (dynamic color, color roles, tonal palettes)
|
||||
- Typography (type scale, fonts)
|
||||
- Elevation and depth
|
||||
- Shapes and corner radius
|
||||
- Icons (Material Symbols)
|
||||
- Motion and transitions
|
||||
|
||||
### 3. Components (`references/components.md`)
|
||||
Read when selecting or implementing:
|
||||
- Action components (buttons, FAB, segmented buttons)
|
||||
- Selection and input (checkbox, radio, switch, text fields, chips)
|
||||
- Navigation (nav bar, drawer, rail, app bars, tabs)
|
||||
- Containment and layout (cards, lists, carousel, sheets)
|
||||
- Communication (dialogs, snackbar, badges, progress, tooltips, menus)
|
||||
|
||||
### 4. M3 Expressive (`references/m3-expressive.md`)
|
||||
Read when creating more engaging experiences:
|
||||
- Expressive motion tactics
|
||||
- Shape morphing
|
||||
- Dynamic animations
|
||||
- Brand expression
|
||||
- Balancing expressiveness with usability
|
||||
|
||||
## Workflows
|
||||
|
||||
### Workflow 1: Designing a New Interface
|
||||
|
||||
When designing a new product or feature with Material 3:
|
||||
|
||||
1. **Define layout structure**
|
||||
- Read `references/foundations.md` → Layout section
|
||||
- Determine window size classes (compact/medium/expanded)
|
||||
- Choose canonical layout if applicable (list-detail, feed, supporting pane)
|
||||
|
||||
2. **Select components**
|
||||
- Read `references/components.md`
|
||||
- Use Component Selection Guide to choose appropriate components
|
||||
- Review specific component guidelines for usage and specs
|
||||
|
||||
3. **Establish visual style**
|
||||
- Read `references/styles.md`
|
||||
- Define color scheme (dynamic or static)
|
||||
- Set typography scale
|
||||
- Choose shape scale (corner radius values)
|
||||
- Plan motion and transitions
|
||||
|
||||
4. **Apply M3 Expressive (optional)**
|
||||
- Read `references/m3-expressive.md`
|
||||
- Identify key moments for expressive design
|
||||
- Apply emphasized easing and extended durations
|
||||
- Consider shape morphing for transitions
|
||||
- Follow 80/20 rule (80% standard, 20% expressive)
|
||||
|
||||
5. **Validate accessibility**
|
||||
- Read `references/foundations.md` → Accessibility section
|
||||
- Check color contrast (WCAG compliance)
|
||||
- Verify touch targets (minimum 48×48dp)
|
||||
- Ensure keyboard navigation and screen reader support
|
||||
|
||||
### Workflow 2: Reviewing Existing Designs
|
||||
|
||||
When reviewing designs for Material 3 compliance:
|
||||
|
||||
1. **Component compliance**
|
||||
- Read `references/components.md` for relevant components
|
||||
- Check if components follow M3 specifications
|
||||
- Verify proper component usage (e.g., not using filled buttons excessively)
|
||||
- Validate component states (enabled, hover, focus, pressed, disabled)
|
||||
|
||||
2. **Visual consistency**
|
||||
- Read `references/styles.md`
|
||||
- Verify color roles are used correctly
|
||||
- Check typography matches type scale
|
||||
- Validate elevation levels
|
||||
- Review shape consistency (corner radius)
|
||||
|
||||
3. **Accessibility audit**
|
||||
- Read `references/foundations.md` → Accessibility section
|
||||
- Test color contrast ratios
|
||||
- Check touch target sizes
|
||||
- Verify text resizing support
|
||||
- Review focus indicators
|
||||
|
||||
4. **Interaction patterns**
|
||||
- Read `references/foundations.md` → Interaction section
|
||||
- Verify state layers are present
|
||||
- Check gesture support for mobile
|
||||
- Validate selection patterns
|
||||
|
||||
### Workflow 3: Generating Design Tokens
|
||||
|
||||
When creating design tokens for a new theme:
|
||||
|
||||
1. **Color tokens**
|
||||
- Read `references/styles.md` → Color section
|
||||
- Choose color scheme type (dynamic or static)
|
||||
- Define source color(s)
|
||||
- Generate tonal palette (13 tones per key color)
|
||||
- Map color roles (primary, secondary, tertiary, surface, error)
|
||||
- Create light and dark theme variants
|
||||
|
||||
2. **Typography tokens**
|
||||
- Read `references/styles.md` → Typography section
|
||||
- Select font family
|
||||
- Define type scale (display, headline, title, body, label × small/medium/large)
|
||||
- Set letter spacing and line height
|
||||
|
||||
3. **Shape tokens**
|
||||
- Read `references/styles.md` → Shape section
|
||||
- Define corner radius scale (none, extra-small, small, medium, large, extra-large)
|
||||
- Map shapes to component categories
|
||||
|
||||
4. **Motion tokens**
|
||||
- Read `references/styles.md` → Motion section
|
||||
- Define duration values (short, medium, long)
|
||||
- Set easing curves (emphasized, standard)
|
||||
- For M3 Expressive, read `references/m3-expressive.md` → Expressive Motion
|
||||
|
||||
### Workflow 4: Implementing M3 Expressive
|
||||
|
||||
When adding expressive elements to enhance engagement:
|
||||
|
||||
1. **Identify key moments**
|
||||
- Onboarding flows
|
||||
- Primary user actions (FAB, main CTAs)
|
||||
- Screen transitions
|
||||
- Success/completion states
|
||||
|
||||
2. **Apply expressive tactics**
|
||||
- Read `references/m3-expressive.md` → Design Tactics
|
||||
- Use emphasized easing for important transitions
|
||||
- Extend animation durations (400-700ms)
|
||||
- Add exaggerated scale changes
|
||||
- Implement layered/staggered animations
|
||||
- Consider shape morphing
|
||||
|
||||
3. **Balance with usability**
|
||||
- Follow 80/20 rule (most interactions remain standard)
|
||||
- Respect `prefers-reduced-motion`
|
||||
- Avoid excessive motion in productivity contexts
|
||||
- Test on lower-end devices
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### When to Read Each Reference
|
||||
|
||||
| Your Question | Read This |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------- |
|
||||
| "What components should I use for navigation?" | `references/components.md` → Navigation Components |
|
||||
| "How do I create a color scheme?" | `references/styles.md` → Color |
|
||||
| "What are the responsive breakpoints?" | `references/foundations.md` → Layout → Window Size Classes |
|
||||
| "How do I make my design more engaging?" | `references/m3-expressive.md` |
|
||||
| "What's the correct button hierarchy?" | `references/components.md` → Action Components → Buttons |
|
||||
| "How do I ensure accessibility?" | `references/foundations.md` → Accessibility |
|
||||
| "What motion timing should I use?" | `references/styles.md` → Motion |
|
||||
| "How do I implement shape morphing?" | `references/m3-expressive.md` → Shape and Form |
|
||||
|
||||
### Component Quick Selector
|
||||
|
||||
**Actions:**
|
||||
- Primary screen action → FAB or Filled Button
|
||||
- Secondary action → Tonal/Outlined Button
|
||||
- Tertiary action → Text Button
|
||||
- Compact action → Icon Button
|
||||
- Toggle options (2-5) → Segmented Button
|
||||
|
||||
**Input:**
|
||||
- Single choice → Radio Button
|
||||
- Multiple choices → Checkbox
|
||||
- On/Off toggle → Switch
|
||||
- Text input → Text Field
|
||||
- Date/time → Date/Time Picker
|
||||
- Range value → Slider
|
||||
- Tags → Input Chips
|
||||
|
||||
**Navigation:**
|
||||
- Compact screens (<600dp) → Navigation Bar
|
||||
- Medium screens (600-840dp) → Navigation Rail
|
||||
- Large screens (>840dp) → Navigation Drawer
|
||||
- Secondary nav → Tabs or Top App Bar
|
||||
|
||||
**Communication:**
|
||||
- Important decision → Dialog
|
||||
- Quick feedback → Snackbar
|
||||
- Notification count → Badge
|
||||
- Loading status → Progress Indicator
|
||||
- Contextual help → Tooltip
|
||||
- Action list → Menu
|
||||
|
||||
### Design Token Defaults
|
||||
|
||||
**Color (Light Theme):**
|
||||
- Primary: tone 40
|
||||
- On-primary: tone 100 (white)
|
||||
- Primary container: tone 90
|
||||
- Surface: tone 98
|
||||
|
||||
**Typography:**
|
||||
- Display Large: 57sp
|
||||
- Headline Large: 32sp
|
||||
- Title Large: 22sp
|
||||
- Body Large: 16sp
|
||||
- Label Large: 14sp
|
||||
|
||||
**Shape:**
|
||||
- Extra Small: 4dp (chips, checkboxes)
|
||||
- Small: 8dp (small buttons)
|
||||
- Medium: 12dp (cards, standard buttons)
|
||||
- Large: 16dp (FAB)
|
||||
- Extra Large: 28dp (dialogs, sheets)
|
||||
|
||||
**Elevation:**
|
||||
- Level 0: 0dp (standard surface)
|
||||
- Level 1: 1dp (cards)
|
||||
- Level 2: 3dp (search bars)
|
||||
- Level 3: 6dp (FAB)
|
||||
- Level 5: 12dp (modals, dialogs)
|
||||
|
||||
## Best Practices
|
||||
|
||||
### General Principles
|
||||
|
||||
1. **Consistency**: Use design tokens consistently across the product
|
||||
2. **Hierarchy**: Establish clear visual hierarchy through size, color, and spacing
|
||||
3. **Accessibility**: Always meet WCAG 2.1 Level AA standards (minimum)
|
||||
4. **Responsiveness**: Design for all window size classes
|
||||
5. **Platform conventions**: Respect platform-specific patterns when appropriate
|
||||
|
||||
### Common Mistakes to Avoid
|
||||
|
||||
- **Too many filled buttons**: Use only one filled button per screen for primary action
|
||||
- **Ignoring window size classes**: Design must adapt to different screen sizes
|
||||
- **Poor color contrast**: Always validate contrast ratios
|
||||
- **Inconsistent spacing**: Use 4dp grid system throughout
|
||||
- **Overusing M3 Expressive**: Keep 80% standard, 20% expressive
|
||||
- **Small touch targets**: Minimum 48×48dp for all interactive elements
|
||||
- **Unclear component states**: All components must show hover, focus, pressed states
|
||||
|
||||
### Platform-Specific Notes
|
||||
|
||||
**Flutter:**
|
||||
- Use Material 3 theme in `ThemeData(useMaterial3: true)`
|
||||
- Access design tokens via `Theme.of(context)`
|
||||
- Official documentation: https://m3.material.io/develop/flutter
|
||||
|
||||
**Android (Jetpack Compose):**
|
||||
- Use Material3 package
|
||||
- MaterialTheme provides M3 components and tokens
|
||||
- Official documentation: https://m3.material.io/develop/android/jetpack-compose
|
||||
|
||||
**Web:**
|
||||
- Use Material Web Components library
|
||||
- CSS custom properties for design tokens
|
||||
- Official documentation: https://m3.material.io/develop/web
|
||||
|
||||
**Platform-agnostic:**
|
||||
- Export design tokens from Material Theme Builder
|
||||
- Apply M3 principles manually to any framework
|
||||
|
||||
## Tools and Resources
|
||||
|
||||
### Material Theme Builder
|
||||
Web-based tool for creating M3 color schemes and design tokens:
|
||||
- Generate color schemes from source colors
|
||||
- Create light and dark themes
|
||||
- Export tokens for various platforms
|
||||
- URL: https://material-foundation.github.io/material-theme-builder/
|
||||
|
||||
### Material Symbols
|
||||
Variable icon font with 2,500+ icons:
|
||||
- Styles: Outlined, Filled, Rounded, Sharp
|
||||
- Variable features: weight, grade, optical size, fill
|
||||
- URL: https://fonts.google.com/icons
|
||||
|
||||
### Official Documentation
|
||||
- Material Design 3: https://m3.material.io/
|
||||
- Get Started: https://m3.material.io/get-started
|
||||
- Blog (updates and announcements): https://m3.material.io/blog
|
||||
|
||||
## Summary
|
||||
|
||||
This skill enables comprehensive Material Design 3 implementation:
|
||||
|
||||
1. **Read references as needed**: Don't try to memorize everything—reference files exist to be consulted during work
|
||||
2. **Follow workflows**: Use structured workflows for common tasks (designing, reviewing, generating tokens)
|
||||
3. **Start with foundations**: Layout, accessibility, and interaction patterns form the base
|
||||
4. **Build with components**: Use the 38 documented M3 components appropriately
|
||||
5. **Apply styles consistently**: Color, typography, shape, elevation, icons, motion
|
||||
6. **Enhance with M3 Expressive**: Add engaging, emotionally resonant elements where appropriate
|
||||
7. **Validate accessibility**: Always check contrast, touch targets, and keyboard navigation
|
||||
|
||||
Material Design 3 is a complete design system—this skill helps you apply it effectively across all contexts.
|
||||
515
.claude/skills/material-thinking/references/components.md
Normal file
515
.claude/skills/material-thinking/references/components.md
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
# Material 3 Components
|
||||
|
||||
Material 3は38のドキュメント化されたコンポーネントを提供します。各コンポーネントには、概要、ガイドライン、仕様、アクセシビリティのサブページがあります。
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Action Components](#action-components)
|
||||
2. [Selection and Input Components](#selection-and-input-components)
|
||||
3. [Navigation Components](#navigation-components)
|
||||
4. [Containment and Layout Components](#containment-and-layout-components)
|
||||
5. [Communication Components](#communication-components)
|
||||
|
||||
---
|
||||
|
||||
## Action Components
|
||||
|
||||
ユーザーがアクションを実行するためのコンポーネント。
|
||||
|
||||
### Buttons
|
||||
|
||||
#### Common Buttons
|
||||
主要なアクションのための標準的なボタン。
|
||||
|
||||
**Variants:**
|
||||
- **Filled**: 最も高い強調度、プライマリアクション
|
||||
- **Filled Tonal**: 中程度の強調度、セカンダリアクション
|
||||
- **Outlined**: 線のみ、中程度の強調度
|
||||
- **Elevated**: 影付き、強調が必要だがFilledほどではない
|
||||
- **Text**: 最も低い強調度、補助的なアクション
|
||||
|
||||
**Usage Guidelines:**
|
||||
- 1つの画面にFilledボタンは1つまで推奨
|
||||
- ボタンの階層を明確に(Filled > Tonal > Outlined > Text)
|
||||
- 最小タッチターゲット: 48×48dp
|
||||
- ラベルは動詞で開始(例: "保存", "送信", "削除")
|
||||
|
||||
URL: https://m3.material.io/components/buttons/overview
|
||||
|
||||
#### Icon Buttons
|
||||
コンパクトな補助的アクションボタン。
|
||||
|
||||
**Variants:**
|
||||
- Standard
|
||||
- Filled
|
||||
- Filled Tonal
|
||||
- Outlined
|
||||
|
||||
**Usage:**
|
||||
- 繰り返し使用されるアクション(お気に入り、共有、削除)
|
||||
- 限られたスペース
|
||||
- アイコンのみで意味が明確な場合
|
||||
|
||||
URL: https://m3.material.io/components/icon-buttons/overview
|
||||
|
||||
#### Floating Action Button (FAB)
|
||||
画面の主要アクションのための浮遊ボタン。
|
||||
|
||||
**Types:**
|
||||
- **FAB**: 標準的なFAB
|
||||
- **Small FAB**: 小さいFAB
|
||||
- **Large FAB**: 大きいFAB
|
||||
- **Extended FAB**: テキストラベル付きFAB
|
||||
|
||||
**Guidelines:**
|
||||
- 1画面に1つのFAB推奨
|
||||
- 最も重要なアクションのみ
|
||||
- 配置: 通常は右下
|
||||
- スクロール時の動作を考慮(隠す/縮小)
|
||||
|
||||
URL: https://m3.material.io/components/floating-action-button/overview
|
||||
|
||||
#### Segmented Buttons
|
||||
関連するオプションの単一選択または複数選択グループ。
|
||||
|
||||
**Usage:**
|
||||
- ビューの切り替え(リスト/グリッド)
|
||||
- フィルタリング(カテゴリ選択)
|
||||
- 設定オプション
|
||||
|
||||
**Guidelines:**
|
||||
- 2-5個のオプション推奨
|
||||
- 各オプションは簡潔に(1-2語)
|
||||
- アイコン+テキストまたはテキストのみ
|
||||
|
||||
URL: https://m3.material.io/components/segmented-buttons/overview
|
||||
|
||||
---
|
||||
|
||||
## Selection and Input Components
|
||||
|
||||
ユーザーが選択や入力を行うためのコンポーネント。
|
||||
|
||||
### Checkbox
|
||||
リストから複数のアイテムを選択。
|
||||
|
||||
**States:**
|
||||
- Unchecked
|
||||
- Checked
|
||||
- Indeterminate(部分選択)
|
||||
|
||||
**Usage:**
|
||||
- 複数選択
|
||||
- オン/オフ設定(ただしSwitchの方が適切な場合も)
|
||||
- リスト項目の選択
|
||||
|
||||
URL: https://m3.material.io/components/checkbox/guidelines
|
||||
|
||||
### Radio Button
|
||||
セットから1つのオプションを選択。
|
||||
|
||||
**Usage:**
|
||||
- 相互排他的なオプション(1つのみ選択可能)
|
||||
- すべてのオプションを表示する必要がある場合
|
||||
- 2-7個のオプション推奨
|
||||
|
||||
**Guidelines:**
|
||||
- デフォルト選択肢を提供
|
||||
- オプションは垂直に配置推奨
|
||||
- ラベルはクリック可能に
|
||||
|
||||
URL: https://m3.material.io/components/radio-button/overview
|
||||
|
||||
### Switch
|
||||
バイナリのオン/オフ切り替え。
|
||||
|
||||
**Usage:**
|
||||
- 即座に効果が反映される設定
|
||||
- 単一アイテムの有効/無効化
|
||||
- リスト内の個別項目の切り替え
|
||||
|
||||
**vs Checkbox:**
|
||||
- Switch: 即座に効果、状態の切り替え
|
||||
- Checkbox: 保存が必要、複数選択
|
||||
|
||||
URL: https://m3.material.io/components/switch/guidelines
|
||||
|
||||
### Text Fields
|
||||
テキスト入力用のフォームフィールド。
|
||||
|
||||
**Types:**
|
||||
- **Filled**: デフォルト、背景塗りつぶし
|
||||
- **Outlined**: 線のみ、フォーム内で推奨
|
||||
|
||||
**Elements:**
|
||||
- Label: 入力内容の説明
|
||||
- Input text: ユーザー入力
|
||||
- Helper text: 補助的な説明
|
||||
- Error text: エラーメッセージ
|
||||
- Leading/Trailing icons: アイコン
|
||||
|
||||
**Guidelines:**
|
||||
- ラベルは簡潔に
|
||||
- プレースホルダーは補助的な例として使用
|
||||
- エラーは具体的に("無効な入力" ではなく "有効なメールアドレスを入力してください")
|
||||
|
||||
URL: https://m3.material.io/components/text-fields/overview
|
||||
|
||||
### Chips
|
||||
コンパクトな情報要素。
|
||||
|
||||
**Types:**
|
||||
- **Assist**: アクションやヘルプのサジェスト
|
||||
- **Filter**: コンテンツのフィルタリング
|
||||
- **Input**: ユーザー入力(タグ、連絡先)
|
||||
- **Suggestion**: 動的な提案
|
||||
|
||||
**Usage:**
|
||||
- タグや属性の表示
|
||||
- フィルタリングオプション
|
||||
- 選択されたアイテムの表示
|
||||
|
||||
URL: https://m3.material.io/components/chips/guidelines
|
||||
|
||||
### Sliders
|
||||
範囲内の値を選択。
|
||||
|
||||
**Types:**
|
||||
- Continuous: 連続的な値
|
||||
- Discrete: 離散的な値(ステップ付き)
|
||||
|
||||
**Usage:**
|
||||
- 音量、明るさ調整
|
||||
- 価格範囲選択
|
||||
- 数値設定
|
||||
|
||||
URL: https://m3.material.io/components/sliders/specs
|
||||
|
||||
### Date Pickers / Time Pickers
|
||||
日付と時刻の選択。
|
||||
|
||||
**Date Picker Modes:**
|
||||
- Modal: ダイアログ形式
|
||||
- Docked: インライン表示
|
||||
|
||||
**Time Picker Types:**
|
||||
- Dial: ダイヤル形式
|
||||
- Input: テキスト入力形式
|
||||
|
||||
URL: https://m3.material.io/components/date-pickers
|
||||
|
||||
---
|
||||
|
||||
## Navigation Components
|
||||
|
||||
アプリ内のナビゲーションを提供するコンポーネント。
|
||||
|
||||
### Navigation Bar
|
||||
モバイル向けボトムナビゲーション。
|
||||
|
||||
**Guidelines:**
|
||||
- 3-5個の主要な目的地
|
||||
- アイコン+ラベル(アイコンのみは避ける)
|
||||
- 常に表示(スクロールしても固定)
|
||||
- Compact window size class向け
|
||||
|
||||
URL: https://m3.material.io/components/navigation-bar/overview
|
||||
|
||||
### Navigation Drawer
|
||||
サイドナビゲーション。
|
||||
|
||||
**Types:**
|
||||
- **Standard**: 画面端から開閉
|
||||
- **Modal**: オーバーレイ形式
|
||||
|
||||
**Usage:**
|
||||
- 5個以上の目的地
|
||||
- Medium/Expanded window size class
|
||||
- アプリの主要セクション
|
||||
|
||||
URL: https://m3.material.io/components/navigation-drawer/overview
|
||||
|
||||
### Navigation Rail
|
||||
垂直方向のナビゲーション(中型画面)。
|
||||
|
||||
**Usage:**
|
||||
- Medium window size class(タブレット縦向き)
|
||||
- 3-7個の目的地
|
||||
- 画面左端に固定
|
||||
|
||||
URL: https://m3.material.io/components/navigation-rail/overview
|
||||
|
||||
### Top App Bar
|
||||
画面上部のタイトルとアクション。
|
||||
|
||||
**Types:**
|
||||
- **Small**: 標準的なアプリバー
|
||||
- **Medium**: 中サイズ(スクロールで縮小)
|
||||
- **Large**: 大サイズ(スクロールで縮小)
|
||||
|
||||
**Elements:**
|
||||
- Navigation icon: 戻る、メニュー
|
||||
- Title: 画面タイトル
|
||||
- Action icons: 主要なアクション(最大3つ推奨)
|
||||
|
||||
URL: https://m3.material.io/components/app-bars/overview
|
||||
|
||||
### Tabs
|
||||
コンテンツを複数のビューに整理。
|
||||
|
||||
**Types:**
|
||||
- Primary tabs: メインコンテンツの切り替え
|
||||
- Secondary tabs: サブセクションの切り替え
|
||||
|
||||
**Guidelines:**
|
||||
- 2-6個のタブ推奨
|
||||
- ラベルは簡潔に(1-2語)
|
||||
- スワイプジェスチャーでの切り替えをサポート
|
||||
|
||||
URL: https://m3.material.io/components/tabs/guidelines
|
||||
|
||||
---
|
||||
|
||||
## Containment and Layout Components
|
||||
|
||||
コンテンツを整理・表示するためのコンポーネント。
|
||||
|
||||
### Cards
|
||||
関連情報をまとめたコンテナ。
|
||||
|
||||
**Types:**
|
||||
- **Elevated**: 影付き
|
||||
- **Filled**: 背景塗りつぶし
|
||||
- **Outlined**: 線のみ
|
||||
|
||||
**Usage:**
|
||||
- 異なるコンテンツのコレクション
|
||||
- アクション可能なコンテンツ
|
||||
- エントリーポイント
|
||||
|
||||
**Guidelines:**
|
||||
- 過度に使用しない(リストで十分な場合も)
|
||||
- 明確なアクションを提供
|
||||
- 情報の階層を維持
|
||||
|
||||
URL: https://m3.material.io/components/cards/guidelines
|
||||
|
||||
### Lists
|
||||
垂直方向のテキストと画像のインデックス。
|
||||
|
||||
**Types:**
|
||||
- Single-line
|
||||
- Two-line
|
||||
- Three-line
|
||||
|
||||
**Elements:**
|
||||
- Leading element: アイコン、画像、チェックボックス
|
||||
- Primary text: メインテキスト
|
||||
- Secondary text: サブテキスト
|
||||
- Trailing element: メタ情報、アクション
|
||||
|
||||
**Usage:**
|
||||
- 同質なコンテンツのコレクション
|
||||
- スキャン可能な情報
|
||||
- 詳細へのエントリーポイント
|
||||
|
||||
URL: https://m3.material.io/components/lists/overview
|
||||
|
||||
### Carousel
|
||||
スクロール可能なビジュアルアイテムのコレクション。
|
||||
|
||||
**Types:**
|
||||
- Hero: 大きい、フォーカスされたアイテム
|
||||
- Multi-browse: 複数アイテム表示
|
||||
- Uncontained: フルブリード
|
||||
|
||||
**Usage:**
|
||||
- 画像ギャラリー
|
||||
- プロダクトショーケース
|
||||
- オンボーディング
|
||||
|
||||
URL: https://m3.material.io/components/carousel/overview
|
||||
|
||||
### Bottom Sheets / Side Sheets
|
||||
追加コンテンツを表示するサーフェス。
|
||||
|
||||
**Types:**
|
||||
- **Standard**: 永続的、画面の一部
|
||||
- **Modal**: 一時的、フォーカスが必要
|
||||
|
||||
**Bottom Sheet Usage:**
|
||||
- コンテキストアクション
|
||||
- 追加オプション
|
||||
- Mobile向け
|
||||
|
||||
**Side Sheet Usage:**
|
||||
- 詳細情報、フィルタ
|
||||
- Tablet/Desktop向け
|
||||
|
||||
URL: https://m3.material.io/components/bottom-sheets/overview
|
||||
|
||||
---
|
||||
|
||||
## Communication Components
|
||||
|
||||
ユーザーにフィードバックや情報を伝えるコンポーネント。
|
||||
|
||||
### Dialogs
|
||||
ユーザーアクションが必要な重要なプロンプト。
|
||||
|
||||
**Types:**
|
||||
- **Basic**: タイトル、本文、アクション
|
||||
- **Full-screen**: フルスクリーンダイアログ(モバイル)
|
||||
|
||||
**Usage:**
|
||||
- 重要な決定(削除確認など)
|
||||
- 必須の情報入力
|
||||
- エラーや警告
|
||||
|
||||
**Guidelines:**
|
||||
- タイトルは質問形式推奨
|
||||
- アクションは明確に("削除"、"キャンセル")
|
||||
- 破壊的なアクションは右側に配置しない
|
||||
|
||||
URL: https://m3.material.io/components/dialogs/guidelines
|
||||
|
||||
### Snackbar
|
||||
プロセスの簡潔な更新を画面下部に表示。
|
||||
|
||||
**Usage:**
|
||||
- 操作完了の確認("メッセージを送信しました")
|
||||
- 軽微なエラー通知
|
||||
- オプショナルなアクション提供
|
||||
|
||||
**Guidelines:**
|
||||
- 表示時間: 4-10秒
|
||||
- 1行のメッセージ推奨
|
||||
- 最大1つのアクション
|
||||
- 重要な情報には使用しない(Dialogを使用)
|
||||
|
||||
URL: https://m3.material.io/components/snackbar/overview
|
||||
|
||||
### Badges
|
||||
ナビゲーション項目上の通知とカウント。
|
||||
|
||||
**Types:**
|
||||
- Numeric: 数値表示(1-999)
|
||||
- Dot: ドット表示(新着あり)
|
||||
|
||||
**Usage:**
|
||||
- 未読通知の数
|
||||
- 新着コンテンツのインジケーター
|
||||
|
||||
URL: https://m3.material.io/components/badges/overview
|
||||
|
||||
### Progress Indicators
|
||||
進行中のプロセスのステータス表示。
|
||||
|
||||
**Types:**
|
||||
- **Circular**: 円形、不定期または確定的
|
||||
- **Linear**: 線形、確定的な進捗
|
||||
|
||||
**Usage:**
|
||||
- Circular: ローディング、処理中
|
||||
- Linear: ファイルアップロード、ダウンロード
|
||||
|
||||
**Guidelines:**
|
||||
- 2秒以上かかる処理で表示
|
||||
- 可能な限り確定的な進捗を使用
|
||||
- 進捗率がわからない場合は不定期
|
||||
|
||||
URL: https://m3.material.io/components/progress-indicators/overview
|
||||
|
||||
### Tooltips
|
||||
コンテキストラベルとメッセージ。
|
||||
|
||||
**Types:**
|
||||
- Plain: テキストのみ
|
||||
- Rich: テキスト+アイコン/画像
|
||||
|
||||
**Usage:**
|
||||
- アイコンボタンの説明
|
||||
- 切り詰められたテキストの完全版
|
||||
- 補助的な情報
|
||||
|
||||
**Guidelines:**
|
||||
- 簡潔に(1行推奨)
|
||||
- 重要な情報には使用しない
|
||||
- タッチデバイスではlong press
|
||||
|
||||
URL: https://m3.material.io/components/tooltips/guidelines
|
||||
|
||||
### Menus
|
||||
一時的なサーフェース上の選択肢リスト。
|
||||
|
||||
**Types:**
|
||||
- Standard menu
|
||||
- Dropdown menu
|
||||
- Exposed dropdown menu(選択状態を表示)
|
||||
|
||||
**Usage:**
|
||||
- コンテキストメニュー
|
||||
- 選択オプション
|
||||
- アクションのリスト
|
||||
|
||||
**Guidelines:**
|
||||
- 2-7個のアイテム推奨
|
||||
- アイコンはオプション
|
||||
- 破壊的なアクションは分離
|
||||
|
||||
URL: https://m3.material.io/components/menus/overview
|
||||
|
||||
### Search
|
||||
検索バーとサジェスト。
|
||||
|
||||
**Elements:**
|
||||
- Search bar: 検索入力フィールド
|
||||
- Search view: 全画面検索インターフェース
|
||||
|
||||
**Usage:**
|
||||
- アプリ内検索
|
||||
- フィルタリング
|
||||
- サジェスト表示
|
||||
|
||||
URL: https://m3.material.io/components/search/overview
|
||||
|
||||
---
|
||||
|
||||
## Component Selection Guide
|
||||
|
||||
### Action Selection
|
||||
|
||||
| Need | Component |
|
||||
|------|-----------|
|
||||
| Primary screen action | FAB or Filled Button |
|
||||
| Secondary action | Tonal/Outlined Button |
|
||||
| Tertiary action | Text Button |
|
||||
| Compact action | Icon Button |
|
||||
| Toggle between 2-5 options | Segmented Button |
|
||||
|
||||
### Input Selection
|
||||
|
||||
| Need | Component |
|
||||
|------|-----------|
|
||||
| Single choice from list | Radio Button |
|
||||
| Multiple choices | Checkbox |
|
||||
| On/Off toggle | Switch |
|
||||
| Text input | Text Field |
|
||||
| Date selection | Date Picker |
|
||||
| Value from range | Slider |
|
||||
| Tags or attributes | Input Chips |
|
||||
|
||||
### Navigation Selection
|
||||
|
||||
| Window Size | Primary Nav | Secondary Nav |
|
||||
|-------------|-------------|---------------|
|
||||
| Compact (<600dp) | Navigation Bar | Tabs |
|
||||
| Medium (600-840dp) | Navigation Rail | Tabs |
|
||||
| Expanded (>840dp) | Navigation Drawer | Tabs, Top App Bar |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Material Design 3 Components: https://m3.material.io/components/
|
||||
- All Components List: https://m3.material.io/components/all-buttons
|
||||
207
.claude/skills/material-thinking/references/foundations.md
Normal file
207
.claude/skills/material-thinking/references/foundations.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
# Material 3 Foundations
|
||||
|
||||
Material 3 Foundationsは、すべてのMaterialインターフェースの基盤となる設計原則とパターンを定義します。
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Accessibility](#accessibility)
|
||||
2. [Layout](#layout)
|
||||
3. [Interaction](#interaction)
|
||||
4. [Content Design](#content-design)
|
||||
5. [Design Tokens](#design-tokens)
|
||||
6. [Adaptive Design](#adaptive-design)
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
### Core Principles
|
||||
- 多様な能力を持つユーザーのための設計
|
||||
- スクリーンリーダーなどの支援技術との統合
|
||||
- WCAG準拠のコントラスト比
|
||||
|
||||
### Key Areas
|
||||
|
||||
#### Structure and Elements
|
||||
- 直感的なレイアウト階層
|
||||
- アクセシブルなUI要素の設計
|
||||
- フォーカス管理とナビゲーション
|
||||
|
||||
URL: https://m3.material.io/foundations/designing/structure
|
||||
|
||||
#### Color Contrast
|
||||
- WCAG準拠のカラーコントラスト
|
||||
- テキストとUIコントロールの視認性
|
||||
- 4.5:1(通常テキスト)、3:1(大きいテキスト、UIコンポーネント)
|
||||
|
||||
URL: https://m3.material.io/foundations/designing/color-contrast
|
||||
|
||||
#### Text Accessibility
|
||||
- テキストリサイズのサポート(200%まで)
|
||||
- アクセシブルなテキスト切り詰め
|
||||
- 明確で適応可能な文章
|
||||
|
||||
URL: https://m3.material.io/foundations/writing/text-resizing
|
||||
|
||||
---
|
||||
|
||||
## Layout
|
||||
|
||||
### Understanding Layout
|
||||
|
||||
#### Core Components
|
||||
- **Regions**: 画面の主要エリア(ヘッダー、本文、ナビゲーション)
|
||||
- **Columns**: グリッドシステムの基本単位
|
||||
- **Gutters**: カラム間のスペース
|
||||
- **Spacing**: 4dpベースの一貫したスペーシングシステム
|
||||
|
||||
URL: https://m3.material.io/foundations/layout/understanding-layout/overview
|
||||
|
||||
### Window Size Classes
|
||||
|
||||
画面サイズに応じたレスポンシブデザイン:
|
||||
|
||||
| Size Class | Width | Typical Device | Key Patterns |
|
||||
|-----------|-------|---------------|--------------|
|
||||
| Compact | <600dp | Phone | Single pane, bottom nav |
|
||||
| Medium | 600-840dp | Tablet (portrait) | Dual pane optional, nav rail |
|
||||
| Expanded | >840dp | Tablet (landscape), Desktop | Dual/multi pane, nav drawer |
|
||||
| Large/XL | >1240dp | Large screens, TV | Multi-pane, extensive nav |
|
||||
|
||||
URL: https://m3.material.io/foundations/layout/applying-layout/window-size-classes
|
||||
|
||||
### Canonical Layouts
|
||||
|
||||
よく使われるレイアウトパターン:
|
||||
|
||||
1. **List-detail**: マスター・詳細ナビゲーション
|
||||
2. **Feed**: コンテンツフィード
|
||||
3. **Supporting pane**: 補助コンテンツパネル
|
||||
|
||||
URL: https://m3.material.io/foundations/layout/canonical-layouts/overview
|
||||
|
||||
---
|
||||
|
||||
## Interaction
|
||||
|
||||
### States
|
||||
|
||||
#### Visual States
|
||||
- **Enabled**: デフォルト状態
|
||||
- **Hover**: ポインタがホバーしている状態(デスクトップ)
|
||||
- **Focused**: キーボードフォーカス
|
||||
- **Pressed**: アクティブに押されている状態
|
||||
- **Dragged**: ドラッグ中
|
||||
- **Disabled**: 無効化状態
|
||||
|
||||
#### State Layers
|
||||
半透明なオーバーレイで状態を視覚的に示す:
|
||||
- Hover: 8% opacity
|
||||
- Focus: 12% opacity
|
||||
- Press: 12% opacity
|
||||
|
||||
URL: https://m3.material.io/foundations/interaction/states/state-layers
|
||||
|
||||
### Gestures
|
||||
|
||||
モバイルインターフェース向けタッチジェスチャー:
|
||||
- Tap: 基本的な選択
|
||||
- Long press: コンテキストメニュー
|
||||
- Drag: 移動、並べ替え
|
||||
- Swipe: ナビゲーション、削除
|
||||
- Pinch: ズーム
|
||||
|
||||
URL: https://m3.material.io/foundations/interaction/gestures
|
||||
|
||||
### Selection
|
||||
|
||||
選択インタラクションパターン:
|
||||
- **Single selection**: ラジオボタン、リスト項目
|
||||
- **Multi selection**: チェックボックス、選択可能なリスト
|
||||
|
||||
URL: https://m3.material.io/foundations/interaction/selection
|
||||
|
||||
---
|
||||
|
||||
## Content Design
|
||||
|
||||
### UX Writing Principles
|
||||
|
||||
1. **Clear**: 明確で理解しやすい
|
||||
2. **Concise**: 簡潔で要点を押さえた
|
||||
3. **Useful**: ユーザーのニーズに応える
|
||||
4. **Consistent**: 用語とトーンの一貫性
|
||||
|
||||
### Notifications
|
||||
|
||||
効果的な通知コンテンツ:
|
||||
- アクション可能な情報
|
||||
- 明確な次のステップ
|
||||
- ユーザーコンテキストの理解
|
||||
|
||||
URL: https://m3.material.io/foundations/content-design/notifications
|
||||
|
||||
### Alt Text
|
||||
|
||||
アクセシブルな画像説明:
|
||||
- 装飾的画像: 空のalt属性
|
||||
- 機能的画像: アクションを説明
|
||||
- 情報的画像: 内容を簡潔に説明
|
||||
|
||||
URL: https://m3.material.io/foundations/content-design/alt-text
|
||||
|
||||
### Global Writing
|
||||
|
||||
国際的なオーディエンス向けの文章:
|
||||
- ローカライゼーションを考慮した単語選択
|
||||
- 文化的に中立な表現
|
||||
- 翻訳しやすい文法構造
|
||||
|
||||
URL: https://m3.material.io/foundations/content-design/global-writing/overview
|
||||
|
||||
---
|
||||
|
||||
## Design Tokens
|
||||
|
||||
### What are Design Tokens?
|
||||
|
||||
デザイントークンは、デザイン、ツール、コード全体で使用される設計上の決定の最小単位:
|
||||
|
||||
- **Color tokens**: primary, secondary, surface, error など
|
||||
- **Typography tokens**: displayLarge, bodyMedium など
|
||||
- **Shape tokens**: cornerRadius, roundedCorner など
|
||||
- **Motion tokens**: duration, easing curves
|
||||
|
||||
### Benefits
|
||||
|
||||
- デザインとコード間の一貫性
|
||||
- テーマのカスタマイズが容易
|
||||
- プラットフォーム間での統一
|
||||
|
||||
URL: https://m3.material.io/foundations/design-tokens/overview
|
||||
|
||||
---
|
||||
|
||||
## Adaptive Design
|
||||
|
||||
### Principles
|
||||
|
||||
- **Responsive**: ウィンドウサイズに応じた調整
|
||||
- **Adaptive**: デバイス特性に応じた最適化
|
||||
- **Contextual**: 使用コンテキストを考慮
|
||||
|
||||
### Key Strategies
|
||||
|
||||
1. Window size classesに基づくレイアウト調整
|
||||
2. 入力方式(タッチ、マウス、キーボード)への対応
|
||||
3. デバイス機能(カメラ、位置情報等)の活用
|
||||
4. オフラインとオンラインシナリオの対応
|
||||
|
||||
URL: https://m3.material.io/foundations/adaptive-design
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Material Design 3 Foundations: https://m3.material.io/foundations/
|
||||
- Glossary: https://m3.material.io/foundations/glossary
|
||||
470
.claude/skills/material-thinking/references/m3-expressive.md
Normal file
470
.claude/skills/material-thinking/references/m3-expressive.md
Normal file
|
|
@ -0,0 +1,470 @@
|
|||
# Material 3 Expressive
|
||||
|
||||
M3 Expressiveは、Googleが2024-2025年に導入したMaterial 3の進化版で、より魅力的で感情的に共鳴するインターフェースを実現します。
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Usability Principles](#usability-principles)
|
||||
3. [Design Tactics](#design-tactics)
|
||||
4. [Expressive Motion](#expressive-motion)
|
||||
5. [Shape and Form](#shape-and-form)
|
||||
6. [Implementation Guidelines](#implementation-guidelines)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
### What is M3 Expressive?
|
||||
|
||||
M3 Expressiveは、標準のMaterial 3を拡張し、以下を実現します:
|
||||
|
||||
- **Engaging**: ユーザーの注意を引き、関心を維持
|
||||
- **Emotionally resonant**: 感情的なつながりを生む
|
||||
- **User-friendly**: 使いやすさを犠牲にしない
|
||||
- **Brand expression**: ブランドの個性を表現
|
||||
|
||||
### Key Differences from Standard M3
|
||||
|
||||
| Aspect | Standard M3 | M3 Expressive |
|
||||
|--------|-------------|---------------|
|
||||
| Motion | 控えめ、機能的 | 大胆、表現豊か |
|
||||
| Shapes | 一貫した角丸 | 動的な形状変形 |
|
||||
| Emphasis | 明確、シンプル | ドラマチック、インパクト |
|
||||
| Timing | 速い(200-300ms) | やや長め(400-700ms) |
|
||||
|
||||
URL: https://m3.material.io/blog/building-with-m3-expressive
|
||||
|
||||
---
|
||||
|
||||
## Usability Principles
|
||||
|
||||
### Creating Engaging Products
|
||||
|
||||
M3 Expressiveは、以下のusability原則に基づきます:
|
||||
|
||||
#### 1. Guide Users
|
||||
ユーザーを適切に誘導する:
|
||||
- **Motion paths**: アニメーションでフローを示す
|
||||
- **Visual hierarchy**: 動きで注意を引く
|
||||
- **Staged reveal**: 段階的に情報を開示
|
||||
|
||||
#### 2. Emphasize Actions
|
||||
重要なアクションを強調:
|
||||
- **Scale changes**: サイズ変化で重要性を示す
|
||||
- **Color dynamics**: 色の変化で状態を表現
|
||||
- **Focused attention**: 1つの要素に注意を集中
|
||||
|
||||
#### 3. Provide Feedback
|
||||
ユーザーのアクションに対する明確なフィードバック:
|
||||
- **Immediate response**: 即座の視覚的反応
|
||||
- **State transitions**: 状態変化を明確に表現
|
||||
- **Completion signals**: アクション完了を示す
|
||||
|
||||
URL: https://m3.material.io/foundations/usability/overview
|
||||
|
||||
---
|
||||
|
||||
## Design Tactics
|
||||
|
||||
M3 Expressiveを実装するための具体的なデザイン戦術。
|
||||
|
||||
URL: https://m3.material.io/foundations/usability/applying-m-3-expressive
|
||||
|
||||
### 1. Emphasized Easing
|
||||
|
||||
**Standard easing**よりも劇的な**Emphasized easing**を使用:
|
||||
|
||||
```
|
||||
Emphasized Decelerate: cubic-bezier(0.05, 0.7, 0.1, 1.0)
|
||||
Emphasized Accelerate: cubic-bezier(0.3, 0.0, 0.8, 0.15)
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- 重要なトランジション
|
||||
- ユーザーの注意を引く必要がある場合
|
||||
- ブランド表現を強化したい場合
|
||||
|
||||
**Example:**
|
||||
```css
|
||||
.expressive-enter {
|
||||
animation: enter 500ms cubic-bezier(0.05, 0.7, 0.1, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Extended Duration
|
||||
|
||||
標準より長いアニメーション時間:
|
||||
|
||||
| Element | Standard | Expressive |
|
||||
|---------|----------|------------|
|
||||
| Small changes | 100ms | 150-200ms |
|
||||
| Medium changes | 250ms | 400-500ms |
|
||||
| Large transitions | 300ms | 500-700ms |
|
||||
|
||||
**Caution:** 1000msを超えないこと
|
||||
|
||||
### 3. Exaggerated Scale
|
||||
|
||||
スケール変化を誇張:
|
||||
|
||||
**Standard:**
|
||||
- Scale: 1.0 → 1.05(+5%)
|
||||
|
||||
**Expressive:**
|
||||
- Scale: 1.0 → 1.15(+15%)
|
||||
- Scale: 1.0 → 0.9 → 1.1(bounce effect)
|
||||
|
||||
**Example use case:**
|
||||
- FABのタップアニメーション
|
||||
- カードの選択状態
|
||||
- アイコンのアクティブ状態
|
||||
|
||||
### 4. Dynamic Color Transitions
|
||||
|
||||
色の動的な変化:
|
||||
|
||||
**Techniques:**
|
||||
- Gradient animations: グラデーションの動的変化
|
||||
- Color pulse: 色のパルス効果
|
||||
- Hue rotation: 色相の変化
|
||||
|
||||
**Example:**
|
||||
```css
|
||||
.expressive-button:active {
|
||||
background: linear-gradient(45deg, primary, tertiary);
|
||||
transition: background 400ms cubic-bezier(0.05, 0.7, 0.1, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Layered Motion
|
||||
|
||||
複数の要素が異なるタイミングで動く:
|
||||
|
||||
**Stagger animations:**
|
||||
- 遅延: 50-100ms per item
|
||||
- リストアイテムの順次表示
|
||||
- カードグリッドの表示
|
||||
|
||||
**Example timing:**
|
||||
```
|
||||
Item 1: 0ms
|
||||
Item 2: 80ms
|
||||
Item 3: 160ms
|
||||
Item 4: 240ms
|
||||
```
|
||||
|
||||
### 6. Shape Morphing
|
||||
|
||||
形状の動的な変形(後述)
|
||||
|
||||
---
|
||||
|
||||
## Expressive Motion
|
||||
|
||||
M3 Expressiveの中核となるモーションシステム。
|
||||
|
||||
URL: https://m3.material.io/blog/m3-expressive-motion-theming
|
||||
|
||||
### Motion Theming System
|
||||
|
||||
カスタマイズ可能な新しいモーションテーマシステム:
|
||||
|
||||
#### Motion Tokens
|
||||
|
||||
**Duration tokens:**
|
||||
```
|
||||
motion.duration.short: 150ms
|
||||
motion.duration.medium: 400ms
|
||||
motion.duration.long: 600ms
|
||||
motion.duration.extra-long: 1000ms
|
||||
```
|
||||
|
||||
**Easing tokens:**
|
||||
```
|
||||
motion.easing.emphasized: cubic-bezier(0.05, 0.7, 0.1, 1.0)
|
||||
motion.easing.emphasizedDecelerate: cubic-bezier(0.05, 0.7, 0.1, 1.0)
|
||||
motion.easing.emphasizedAccelerate: cubic-bezier(0.3, 0.0, 0.8, 0.15)
|
||||
motion.easing.standard: cubic-bezier(0.2, 0.0, 0, 1.0)
|
||||
```
|
||||
|
||||
### Expressive Transition Patterns
|
||||
|
||||
#### 1. Container Transform (Enhanced)
|
||||
|
||||
**Standard container transform:**
|
||||
- Duration: 300ms
|
||||
- Easing: standard
|
||||
|
||||
**Expressive container transform:**
|
||||
- Duration: 500ms
|
||||
- Easing: emphasized
|
||||
- 追加効果: 軽いスケール変化、色の変化
|
||||
|
||||
#### 2. Shared Axis (Enhanced)
|
||||
|
||||
**Expressive enhancements:**
|
||||
- より大きいスライド距離(+20%)
|
||||
- フェード+スケール効果の組み合わせ
|
||||
- ステージングされた要素の動き
|
||||
|
||||
#### 3. Morph Transition
|
||||
|
||||
新しいトランジションタイプ:
|
||||
- 形状の滑らかな変形
|
||||
- 複数プロパティの同時変化(サイズ、色、形状)
|
||||
- 有機的な動き
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Circle → Rounded Rectangle → Full Screen
|
||||
(300ms) → (200ms)
|
||||
```
|
||||
|
||||
### Micro-interactions
|
||||
|
||||
小さいが印象的なインタラクション:
|
||||
|
||||
#### Button Press
|
||||
```
|
||||
1. Scale down: 0.95 (50ms)
|
||||
2. Scale up: 1.0 (150ms, emphasized easing)
|
||||
3. Ripple effect: expanded, slower
|
||||
```
|
||||
|
||||
#### Icon State Change
|
||||
```
|
||||
1. Scale out: 0.8 + rotate 15deg (100ms)
|
||||
2. Icon swap
|
||||
3. Scale in: 1.0 + rotate 0deg (200ms, emphasized)
|
||||
```
|
||||
|
||||
#### Loading States
|
||||
```
|
||||
- Pulse animation: 1.0 → 1.1 → 1.0 (800ms, loop)
|
||||
- Color shift: primary → tertiary → primary
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shape and Form
|
||||
|
||||
### Shape Morph
|
||||
|
||||
動的な形状変形でブランド表現を強化。
|
||||
|
||||
URL: https://m3.material.io/styles/shape/shape-morph
|
||||
|
||||
#### Basic Shape Morph
|
||||
|
||||
形状の滑らかな変化:
|
||||
|
||||
**Example scenarios:**
|
||||
1. **FAB → Dialog**
|
||||
- Circle (56dp) → Rounded rectangle (280×400dp)
|
||||
- Duration: 500ms
|
||||
- Easing: emphasized decelerate
|
||||
|
||||
2. **Chip → Card**
|
||||
- Small rounded (32dp) → Medium rounded (card size)
|
||||
- Duration: 400ms
|
||||
|
||||
3. **Button → Full Width**
|
||||
- Fixed width → Full screen width
|
||||
- Corner radius維持
|
||||
|
||||
#### Advanced Techniques
|
||||
|
||||
**Path morphing:**
|
||||
- SVGパスの変形
|
||||
- ベジェ曲線の補間
|
||||
- 複雑な形状間の遷移
|
||||
|
||||
**Example SVG morph:**
|
||||
```svg
|
||||
<path d="M10,10 L90,10 L90,90 L10,90 Z">
|
||||
<animate attributeName="d"
|
||||
to="M50,10 L90,50 L50,90 L10,50 Z"
|
||||
dur="500ms"
|
||||
fill="freeze"/>
|
||||
</path>
|
||||
```
|
||||
|
||||
### Organic Shapes
|
||||
|
||||
より自然で有機的な形状:
|
||||
|
||||
**Characteristics:**
|
||||
- 非対称な角丸
|
||||
- 流動的なライン
|
||||
- 自然界からのインスピレーション
|
||||
|
||||
**Use cases:**
|
||||
- ブランド要素
|
||||
- ヒーローセクション
|
||||
- イラストレーション
|
||||
|
||||
---
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### When to Use M3 Expressive
|
||||
|
||||
#### Good Use Cases ✓
|
||||
- **Consumer apps**: エンターテイメント、ソーシャル、ゲーム
|
||||
- **Brand-forward products**: ブランド表現が重要
|
||||
- **Engagement-critical flows**: オンボーディング、チュートリアル
|
||||
- **Hero moments**: 重要なマイルストーン、達成
|
||||
|
||||
#### Use with Caution ⚠
|
||||
- **Productivity apps**: 過度なアニメーションは避ける
|
||||
- **Frequent actions**: 繰り返し使用される操作
|
||||
- **Data-heavy interfaces**: 情報が優先される場合
|
||||
|
||||
#### Avoid ✗
|
||||
- **Accessibility concerns**: 動きに敏感なユーザー
|
||||
- **Performance-constrained**: 低スペックデバイス
|
||||
- **Critical tasks**: エラーや警告の表示
|
||||
|
||||
### Balancing Expressiveness and Usability
|
||||
|
||||
#### The 80/20 Rule
|
||||
|
||||
- **80%**: 標準のM3(速く、機能的)
|
||||
- **20%**: M3 Expressive(印象的、ブランド表現)
|
||||
|
||||
**Example distribution:**
|
||||
- Standard M3: リスト項目タップ、フォーム入力、設定変更
|
||||
- M3 Expressive: 画面遷移、主要アクション(FAB)、初回体験
|
||||
|
||||
### Respect User Preferences
|
||||
|
||||
#### Reduced Motion
|
||||
|
||||
`prefers-reduced-motion`メディアクエリを尊重:
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Accessibility
|
||||
|
||||
- **Vestibular disorders**: 大きい動きを避ける
|
||||
- **Cognitive load**: 同時に動く要素を制限
|
||||
- **Focus management**: アニメーション中もフォーカス可能
|
||||
|
||||
---
|
||||
|
||||
## Practical Examples
|
||||
|
||||
### Example 1: Expressive FAB Tap
|
||||
|
||||
```css
|
||||
.fab {
|
||||
transition: transform 150ms cubic-bezier(0.05, 0.7, 0.1, 1.0),
|
||||
box-shadow 150ms cubic-bezier(0.05, 0.7, 0.1, 1.0);
|
||||
}
|
||||
|
||||
.fab:active {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
.fab:not(:active) {
|
||||
transform: scale(1.0);
|
||||
}
|
||||
|
||||
/* Ripple with longer duration */
|
||||
.fab::after {
|
||||
animation: ripple 600ms cubic-bezier(0.05, 0.7, 0.1, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Card to Detail Transition
|
||||
|
||||
```javascript
|
||||
// Container transform with expressive timing
|
||||
const expandCard = (card) => {
|
||||
card.animate([
|
||||
{
|
||||
transform: 'scale(1)',
|
||||
borderRadius: '12px'
|
||||
},
|
||||
{
|
||||
transform: 'scale(1.02)',
|
||||
borderRadius: '28px',
|
||||
offset: 0.3
|
||||
},
|
||||
{
|
||||
transform: 'scale(1)',
|
||||
borderRadius: '0px'
|
||||
}
|
||||
], {
|
||||
duration: 500,
|
||||
easing: 'cubic-bezier(0.05, 0.7, 0.1, 1.0)',
|
||||
fill: 'forwards'
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Example 3: Staggered List Animation
|
||||
|
||||
```css
|
||||
.list-item {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
animation: fadeInUp 400ms cubic-bezier(0.05, 0.7, 0.1, 1.0) forwards;
|
||||
}
|
||||
|
||||
.list-item:nth-child(1) { animation-delay: 0ms; }
|
||||
.list-item:nth-child(2) { animation-delay: 80ms; }
|
||||
.list-item:nth-child(3) { animation-delay: 160ms; }
|
||||
.list-item:nth-child(4) { animation-delay: 240ms; }
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resources and Tools
|
||||
|
||||
### Design Tools
|
||||
- **Material Theme Builder**: M3 Expressiveモーションプリセット
|
||||
- **Figma Plugins**: Motion timing visualization
|
||||
- **After Effects**: プロトタイプアニメーション
|
||||
|
||||
### Code Libraries
|
||||
- **Web**: Material Web Components (M3 support)
|
||||
- **Flutter**: Material 3 with custom motion
|
||||
- **Android**: Jetpack Compose Material3
|
||||
|
||||
### References
|
||||
- M3 Expressive announcement: https://m3.material.io/blog/building-with-m3-expressive
|
||||
- Motion theming: https://m3.material.io/blog/m3-expressive-motion-theming
|
||||
- Usability tactics: https://m3.material.io/foundations/usability/applying-m-3-expressive
|
||||
|
||||
---
|
||||
|
||||
## Summary Checklist
|
||||
|
||||
When implementing M3 Expressive, ensure:
|
||||
|
||||
- [ ] Emphasized easing for key transitions
|
||||
- [ ] Extended durations (but <1000ms)
|
||||
- [ ] Exaggerated scale changes where appropriate
|
||||
- [ ] Layered/staggered animations for lists
|
||||
- [ ] Shape morphing for container transforms
|
||||
- [ ] Color dynamics for feedback
|
||||
- [ ] Respect `prefers-reduced-motion`
|
||||
- [ ] 80/20 balance (Standard M3 vs Expressive)
|
||||
- [ ] Test on lower-end devices
|
||||
- [ ] Validate accessibility
|
||||
318
.claude/skills/material-thinking/references/styles.md
Normal file
318
.claude/skills/material-thinking/references/styles.md
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
# Material 3 Styles
|
||||
|
||||
Material 3 Stylesは、カラー、タイポグラフィ、形状、エレベーション、アイコン、モーションを通じて視覚言語を定義します。
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Color](#color)
|
||||
2. [Typography](#typography)
|
||||
3. [Elevation](#elevation)
|
||||
4. [Shape](#shape)
|
||||
5. [Icons](#icons)
|
||||
6. [Motion](#motion)
|
||||
|
||||
---
|
||||
|
||||
## Color
|
||||
|
||||
### Color System Overview
|
||||
|
||||
Material 3のカラーシステムは、アクセシブルでパーソナライズ可能なカラースキームを作成します。
|
||||
|
||||
URL: https://m3.material.io/styles/color/system/overview
|
||||
|
||||
### Color Roles
|
||||
|
||||
UIエレメントを特定の色に結びつける役割:
|
||||
|
||||
#### Primary Colors
|
||||
- **primary**: アプリの主要色(メインボタン、アクティブ状態)
|
||||
- **onPrimary**: プライマリ色上のテキスト/アイコン
|
||||
- **primaryContainer**: プライマリ要素のコンテナ
|
||||
- **onPrimaryContainer**: コンテナ上のテキスト
|
||||
|
||||
#### Secondary & Tertiary
|
||||
- **secondary**: アクセントカラー
|
||||
- **tertiary**: 強調やバランス調整
|
||||
|
||||
#### Surface Colors
|
||||
- **surface**: カード、シート、メニューの背景
|
||||
- **surfaceVariant**: わずかに異なる背景
|
||||
- **surfaceTint**: エレベーション表現用
|
||||
|
||||
#### Semantic Colors
|
||||
- **error**: エラー状態
|
||||
- **warning**: 警告(一部実装で利用可能)
|
||||
- **success**: 成功状態(一部実装で利用可能)
|
||||
|
||||
URL: https://m3.material.io/styles/color/roles
|
||||
|
||||
### Color Schemes
|
||||
|
||||
#### Dynamic Color
|
||||
ユーザーの壁紙や選択から色を抽出:
|
||||
- **User-generated**: ユーザーの選択から
|
||||
- **Content-based**: 画像/コンテンツから抽出
|
||||
|
||||
URL: https://m3.material.io/styles/color/dynamic-color/overview
|
||||
|
||||
#### Static Color
|
||||
固定されたカラースキーム:
|
||||
- **Baseline**: デフォルトのMaterialベースライン
|
||||
- **Custom brand**: カスタムブランドカラー
|
||||
|
||||
URL: https://m3.material.io/styles/color/static/baseline
|
||||
|
||||
### Key Colors and Tones
|
||||
|
||||
- **Source color**: スキーム生成の起点となる色
|
||||
- **Tonal palette**: 各キーカラーから生成される13段階のトーン(0, 10, 20, ..., 100)
|
||||
- Light theme: 通常トーン40をプライマリに使用
|
||||
- Dark theme: 通常トーン80をプライマリに使用
|
||||
|
||||
URL: https://m3.material.io/styles/color/the-color-system/key-colors-tones
|
||||
|
||||
### Tools
|
||||
|
||||
**Material Theme Builder**: カラースキーム生成、カスタマイズ、エクスポートツール
|
||||
|
||||
URL: https://m3.material.io/blog/material-theme-builder-2-color-match
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
### Type Scale
|
||||
|
||||
Material 3は5つのロール×3つのサイズ = 15のタイプスタイルを定義:
|
||||
|
||||
#### Roles
|
||||
1. **Display**: 大きく短いテキスト(ヒーロー、見出し)
|
||||
2. **Headline**: 中規模の見出し
|
||||
3. **Title**: 小さい見出し(アプリバー、リスト項目)
|
||||
4. **Body**: 本文テキスト
|
||||
5. **Label**: ボタン、タブ、小さいテキスト
|
||||
|
||||
#### Sizes
|
||||
- **Large**: 最大サイズ
|
||||
- **Medium**: 標準サイズ
|
||||
- **Small**: 最小サイズ
|
||||
|
||||
#### Example Styles
|
||||
```
|
||||
displayLarge: 57sp, -0.25 letter spacing
|
||||
headlineMedium: 28sp, 0 letter spacing
|
||||
bodyLarge: 16sp, 0.5 letter spacing
|
||||
labelSmall: 11sp, 0.5 letter spacing
|
||||
```
|
||||
|
||||
URL: https://m3.material.io/styles/typography/overview
|
||||
|
||||
### Fonts
|
||||
|
||||
- デフォルト: **Roboto** (Android), **San Francisco** (iOS), **Roboto** (Web)
|
||||
- カスタムフォントのサポート
|
||||
- 変数フォントの活用
|
||||
|
||||
URL: https://m3.material.io/styles/typography/fonts
|
||||
|
||||
### Applying Typography
|
||||
|
||||
- セマンティックな使用(見出しにはheadline、本文にはbody)
|
||||
- 一貫した階層
|
||||
- 行の高さと余白の適切な設定
|
||||
|
||||
URL: https://m3.material.io/styles/typography/applying-type
|
||||
|
||||
---
|
||||
|
||||
## Elevation
|
||||
|
||||
### Overview
|
||||
|
||||
エレベーションはZ軸上のサーフェス間の距離を表現します。
|
||||
|
||||
URL: https://m3.material.io/styles/elevation/overview
|
||||
|
||||
### Elevation Levels
|
||||
|
||||
Material 3は6つのエレベーションレベルを定義:
|
||||
|
||||
| Level | DP | Use Case |
|
||||
|-------|-----|----------|
|
||||
| 0 | 0dp | 通常のサーフェス |
|
||||
| 1 | 1dp | カード、わずかに浮いた要素 |
|
||||
| 2 | 3dp | 検索バー |
|
||||
| 3 | 6dp | FAB(休止状態) |
|
||||
| 4 | 8dp | ナビゲーションドロワー |
|
||||
| 5 | 12dp | モーダルボトムシート、ダイアログ |
|
||||
|
||||
### Elevation Representation
|
||||
|
||||
Material 3では2つの方法でエレベーションを表現:
|
||||
|
||||
1. **Shadow**: 影によるエレベーション(Light theme主体)
|
||||
2. **Surface tint**: サーフェスに色のティントを重ねる(Dark theme主体)
|
||||
|
||||
URL: https://m3.material.io/styles/elevation/applying-elevation
|
||||
|
||||
---
|
||||
|
||||
## Shape
|
||||
|
||||
### Overview
|
||||
|
||||
形状は、注意の誘導、状態表現、ブランド表現に使用されます。
|
||||
|
||||
URL: https://m3.material.io/styles/shape/overview-principles
|
||||
|
||||
### Corner Radius Scale
|
||||
|
||||
Material 3は5つの形状トークンを定義:
|
||||
|
||||
| Token | Default Value | Use Case |
|
||||
|-------|---------------|----------|
|
||||
| None | 0dp | フルスクリーン、厳格なレイアウト |
|
||||
| Extra Small | 4dp | チェックボックス、小さい要素 |
|
||||
| Small | 8dp | チップ、小さいボタン |
|
||||
| Medium | 12dp | カード、標準ボタン |
|
||||
| Large | 16dp | FAB、大きいカード |
|
||||
| Extra Large | 28dp | ダイアログ、ボトムシート |
|
||||
| Full | 9999dp | 完全な円形 |
|
||||
|
||||
### Shape Morph
|
||||
|
||||
**M3 Expressiveの重要機能**: 形状が滑らかに変形するアニメーション
|
||||
|
||||
- トランジション時の視覚的な流れ
|
||||
- ブランド表現の強化
|
||||
- ユーザーの注意を引く
|
||||
|
||||
URL: https://m3.material.io/styles/shape/shape-morph
|
||||
|
||||
---
|
||||
|
||||
## Icons
|
||||
|
||||
### Material Symbols
|
||||
|
||||
Material Symbolsは可変アイコンフォント:
|
||||
|
||||
#### Styles
|
||||
- **Outlined**: 線のみのスタイル(デフォルト)
|
||||
- **Filled**: 塗りつぶしスタイル
|
||||
- **Rounded**: 丸みを帯びたスタイル
|
||||
- **Sharp**: シャープなスタイル
|
||||
|
||||
#### Variable Features
|
||||
- **Weight**: 線の太さ(100-700)
|
||||
- **Grade**: 視覚的な重み(-25 to 200)
|
||||
- **Optical size**: 表示サイズ最適化(20, 24, 40, 48dp)
|
||||
- **Fill**: 塗りつぶし状態(0-1)
|
||||
|
||||
#### Sizes
|
||||
- 20dp: 密なレイアウト
|
||||
- 24dp: 標準サイズ
|
||||
- 40dp: タッチターゲット拡大
|
||||
- 48dp: 大きいタッチターゲット
|
||||
|
||||
URL: https://m3.material.io/styles/icons/overview
|
||||
|
||||
### Custom Icons
|
||||
|
||||
カスタムアイコンのデザインガイドライン:
|
||||
- 24×24dpグリッド
|
||||
- 2dpストローク幅
|
||||
- 2dpの角丸
|
||||
- 一貫したメタファー
|
||||
|
||||
URL: https://m3.material.io/styles/icons/designing-icons
|
||||
|
||||
---
|
||||
|
||||
## Motion
|
||||
|
||||
**M3 Expressiveの中核要素**: モーションは、UIを表現豊かで使いやすくします。
|
||||
|
||||
URL: https://m3.material.io/styles/motion/overview
|
||||
|
||||
### Motion Principles
|
||||
|
||||
1. **Informative**: ユーザーに情報を伝える
|
||||
2. **Focused**: 注意を適切に誘導
|
||||
3. **Expressive**: 感情的なエンゲージメントを高める
|
||||
|
||||
URL: https://m3.material.io/styles/motion/overview/how-it-works
|
||||
|
||||
### Easing and Duration
|
||||
|
||||
#### Easing Types
|
||||
|
||||
Material 3は4つのイージングカーブを定義:
|
||||
|
||||
1. **Emphasized**: 劇的で表現豊かな動き
|
||||
- Decelerate: cubic-bezier(0.05, 0.7, 0.1, 1.0)
|
||||
- Accelerate: cubic-bezier(0.3, 0.0, 0.8, 0.15)
|
||||
- Standard: cubic-bezier(0.2, 0.0, 0, 1.0)
|
||||
|
||||
2. **Standard**: バランスの取れた標準的な動き
|
||||
- cubic-bezier(0.2, 0.0, 0, 1.0)
|
||||
|
||||
3. **Emphasized Decelerate**: 要素が画面に入る
|
||||
- cubic-bezier(0.05, 0.7, 0.1, 1.0)
|
||||
|
||||
4. **Emphasized Accelerate**: 要素が画面から出る
|
||||
- cubic-bezier(0.3, 0.0, 0.8, 0.15)
|
||||
|
||||
#### Duration Guidelines
|
||||
|
||||
| Element Change | Duration |
|
||||
|----------------|----------|
|
||||
| Small (icon state) | 50-100ms |
|
||||
| Medium (component state) | 250-300ms |
|
||||
| Large (layout change) | 400-500ms |
|
||||
| Complex transition | 500-700ms |
|
||||
|
||||
**重要**: 長すぎるアニメーション(>1000ms)は避ける
|
||||
|
||||
URL: https://m3.material.io/styles/motion/easing-and-duration
|
||||
|
||||
### Transitions
|
||||
|
||||
ナビゲーション時のトランジションパターン:
|
||||
|
||||
#### Transition Types
|
||||
|
||||
1. **Container transform**: コンテナが変形して次の画面へ
|
||||
2. **Shared axis**: 共通軸に沿った移動(X, Y, Z軸)
|
||||
3. **Fade through**: フェードアウト→フェードイン
|
||||
4. **Fade**: シンプルなフェード
|
||||
|
||||
#### When to Use Each
|
||||
|
||||
- **Container transform**: リスト項目→詳細画面
|
||||
- **Shared axis X**: タブ切り替え、水平ナビゲーション
|
||||
- **Shared axis Y**: ステッパー、垂直ナビゲーション
|
||||
- **Shared axis Z**: 前後のナビゲーション(戻る/進む)
|
||||
- **Fade through**: コンテンツ更新(関連性が低い)
|
||||
- **Fade**: オーバーレイ、補助的な変更
|
||||
|
||||
URL: https://m3.material.io/styles/motion/transitions/transition-patterns
|
||||
|
||||
### M3 Expressive Motion
|
||||
|
||||
**新しい表現豊かなモーションシステム**:
|
||||
|
||||
- より大胆なアニメーション
|
||||
- カスタマイズ可能なモーションテーマ
|
||||
- ブランド表現の強化
|
||||
|
||||
URL: https://m3.material.io/blog/m3-expressive-motion-theming
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Material Design 3 Styles: https://m3.material.io/styles/
|
||||
- Material Theme Builder: https://material-foundation.github.io/material-theme-builder/
|
||||
- Material Symbols: https://fonts.google.com/icons
|
||||
201
.claude/skills/moai-platform-supabase/SKILL.md
Normal file
201
.claude/skills/moai-platform-supabase/SKILL.md
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
---
|
||||
name: "moai-platform-supabase"
|
||||
description: "Supabase specialist covering PostgreSQL 16, pgvector, RLS, real-time subscriptions, and Edge Functions. Use when building full-stack apps with Supabase backend."
|
||||
version: 2.0.0
|
||||
category: "platform"
|
||||
modularized: true
|
||||
tags: ['supabase', 'postgresql', 'pgvector', 'realtime', 'rls', 'edge-functions']
|
||||
context7-libraries: ['/supabase/supabase']
|
||||
related-skills: ['moai-platform-neon', 'moai-lang-typescript']
|
||||
updated: 2026-01-06
|
||||
status: "active"
|
||||
allowed-tools: "Read, Grep, Glob, mcp__context7__resolve-library-id, mcp__context7__get-library-docs"
|
||||
---
|
||||
|
||||
# moai-platform-supabase: Supabase Platform Specialist
|
||||
|
||||
## Quick Reference (30 seconds)
|
||||
|
||||
Supabase Full-Stack Platform: PostgreSQL 16 with pgvector for AI/vector search, Row-Level Security for multi-tenant apps, real-time subscriptions, Edge Functions with Deno runtime, and integrated Storage with transformations.
|
||||
|
||||
### Core Capabilities
|
||||
|
||||
PostgreSQL 16: Latest PostgreSQL with full SQL support, JSONB, and advanced features
|
||||
pgvector Extension: AI embeddings storage with HNSW/IVFFlat indexes for similarity search
|
||||
Row-Level Security: Automatic multi-tenant data isolation at database level
|
||||
Real-time Subscriptions: Live data sync via Postgres Changes and Presence
|
||||
Edge Functions: Serverless Deno functions at the edge
|
||||
Storage: File storage with automatic image transformations
|
||||
Auth: Built-in authentication with JWT integration
|
||||
|
||||
### When to Use Supabase
|
||||
|
||||
- Multi-tenant SaaS applications requiring data isolation
|
||||
- AI/ML applications needing vector embeddings and similarity search
|
||||
- Real-time collaborative features (presence, live updates)
|
||||
- Full-stack applications needing auth, database, and storage
|
||||
- Projects requiring PostgreSQL-specific features
|
||||
|
||||
### Context7 Documentation Access
|
||||
|
||||
For latest Supabase API documentation, use the Context7 MCP tools:
|
||||
|
||||
Step 1 - Resolve library ID:
|
||||
Use mcp__context7__resolve-library-id with query "supabase" to get the Context7-compatible library ID
|
||||
|
||||
Step 2 - Fetch documentation:
|
||||
Use mcp__context7__get-library-docs with the resolved library ID, specifying topic and token allocation
|
||||
|
||||
Example topics: "postgresql pgvector", "row-level-security policies", "realtime subscriptions presence", "edge-functions deno", "storage transformations", "auth jwt"
|
||||
|
||||
---
|
||||
|
||||
## Module Index
|
||||
|
||||
This skill uses progressive disclosure with specialized modules for detailed implementation patterns.
|
||||
|
||||
### Core Modules
|
||||
|
||||
**postgresql-pgvector** - PostgreSQL 16 with pgvector extension for AI embeddings and semantic search
|
||||
- Vector storage with 1536-dimension OpenAI embeddings
|
||||
- HNSW and IVFFlat index strategies
|
||||
- Semantic search functions
|
||||
- Hybrid search combining vector and full-text
|
||||
|
||||
**row-level-security** - RLS policies for multi-tenant data isolation
|
||||
- Basic tenant isolation patterns
|
||||
- Hierarchical organization access
|
||||
- Role-based modification policies
|
||||
- Service role bypass for server operations
|
||||
|
||||
**realtime-presence** - Real-time subscriptions and presence tracking
|
||||
- Postgres Changes subscription patterns
|
||||
- Filtered change listeners
|
||||
- Presence state management
|
||||
- Collaborative cursor and typing indicators
|
||||
|
||||
**edge-functions** - Serverless Deno functions at the edge
|
||||
- Basic Edge Function with authentication
|
||||
- CORS header configuration
|
||||
- JWT token verification
|
||||
- Rate limiting implementation
|
||||
|
||||
**storage-cdn** - File storage with image transformations
|
||||
- File upload patterns
|
||||
- Image transformation URLs
|
||||
- Thumbnail generation
|
||||
- Cache control configuration
|
||||
|
||||
**auth-integration** - Authentication patterns and JWT handling
|
||||
- Server-side client creation
|
||||
- Cookie-based session management
|
||||
- Auth state synchronization
|
||||
- Protected route patterns
|
||||
|
||||
**typescript-patterns** - TypeScript client patterns and service layers
|
||||
- Server-side client for Next.js App Router
|
||||
- Service layer abstraction pattern
|
||||
- Subscription management
|
||||
- Type-safe database operations
|
||||
|
||||
---
|
||||
|
||||
## Quick Start Patterns
|
||||
|
||||
### Database Setup
|
||||
|
||||
Enable pgvector extension and create embeddings table:
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
CREATE TABLE documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
content TEXT NOT NULL,
|
||||
embedding vector(1536),
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_documents_embedding ON documents
|
||||
USING hnsw (embedding vector_cosine_ops);
|
||||
```
|
||||
|
||||
### Basic RLS Policy
|
||||
|
||||
```sql
|
||||
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "tenant_isolation" ON projects FOR ALL
|
||||
USING (tenant_id = (auth.jwt() ->> 'tenant_id')::UUID);
|
||||
```
|
||||
|
||||
### Real-time Subscription
|
||||
|
||||
```typescript
|
||||
const channel = supabase.channel('db-changes')
|
||||
.on('postgres_changes',
|
||||
{ event: '*', schema: 'public', table: 'messages' },
|
||||
(payload) => console.log('Change:', payload)
|
||||
)
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
### Edge Function Template
|
||||
|
||||
```typescript
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
|
||||
serve(async (req) => {
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
||||
)
|
||||
// Process request
|
||||
return new Response(JSON.stringify({ success: true }))
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
Performance: Use HNSW indexes for vectors, Supavisor for connection pooling in serverless
|
||||
Security: Always enable RLS, verify JWT tokens, use service_role only in Edge Functions
|
||||
Migration: Use Supabase CLI (supabase migration new, supabase db push)
|
||||
|
||||
---
|
||||
|
||||
## Works Well With
|
||||
|
||||
- moai-platform-neon - Alternative PostgreSQL for specific use cases
|
||||
- moai-lang-typescript - TypeScript patterns for Supabase client
|
||||
- moai-domain-backend - Backend architecture integration
|
||||
- moai-foundation-quality - Security and RLS best practices
|
||||
- moai-workflow-testing - Test-driven development with Supabase
|
||||
|
||||
---
|
||||
|
||||
## Module References
|
||||
|
||||
For detailed implementation patterns, see the modules directory:
|
||||
|
||||
- modules/postgresql-pgvector.md - Complete vector search implementation
|
||||
- modules/row-level-security.md - Multi-tenant RLS patterns
|
||||
- modules/realtime-presence.md - Real-time collaboration features
|
||||
- modules/edge-functions.md - Serverless function patterns
|
||||
- modules/storage-cdn.md - File storage and transformations
|
||||
- modules/auth-integration.md - Authentication patterns
|
||||
- modules/typescript-patterns.md - TypeScript client architecture
|
||||
|
||||
For API reference summary, see reference.md
|
||||
For full-stack templates, see examples.md
|
||||
|
||||
---
|
||||
|
||||
Status: Production Ready
|
||||
Generated with: MoAI-ADK Skill Factory v2.0
|
||||
Last Updated: 2026-01-06
|
||||
Version: 2.0.0 (Modularized)
|
||||
Coverage: PostgreSQL 16, pgvector, RLS, Real-time, Edge Functions, Storage
|
||||
502
.claude/skills/moai-platform-supabase/examples.md
Normal file
502
.claude/skills/moai-platform-supabase/examples.md
Normal file
|
|
@ -0,0 +1,502 @@
|
|||
---
|
||||
name: supabase-examples
|
||||
description: Full-stack templates and working examples for Supabase applications
|
||||
parent-skill: moai-platform-supabase
|
||||
version: 1.0.0
|
||||
updated: 2026-01-06
|
||||
---
|
||||
|
||||
# Supabase Full-Stack Examples
|
||||
|
||||
## Multi-Tenant SaaS Application
|
||||
|
||||
### Database Schema
|
||||
|
||||
```sql
|
||||
-- Organizations (tenants)
|
||||
CREATE TABLE organizations (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
plan TEXT DEFAULT 'free' CHECK (plan IN ('free', 'pro', 'enterprise')),
|
||||
settings JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Organization members with roles
|
||||
CREATE TABLE organization_members (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL,
|
||||
role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
|
||||
joined_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(organization_id, user_id)
|
||||
);
|
||||
|
||||
-- Projects within organizations
|
||||
CREATE TABLE projects (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
owner_id UUID NOT NULL,
|
||||
status TEXT DEFAULT 'active',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Policies
|
||||
CREATE POLICY "org_member_select" ON organizations FOR SELECT
|
||||
USING (id IN (SELECT organization_id FROM organization_members WHERE user_id = auth.uid()));
|
||||
|
||||
CREATE POLICY "org_admin_update" ON organizations FOR UPDATE
|
||||
USING (id IN (SELECT organization_id FROM organization_members
|
||||
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')));
|
||||
|
||||
CREATE POLICY "member_view" ON organization_members FOR SELECT
|
||||
USING (organization_id IN (SELECT organization_id FROM organization_members WHERE user_id = auth.uid()));
|
||||
|
||||
CREATE POLICY "project_access" ON projects FOR ALL
|
||||
USING (organization_id IN (SELECT organization_id FROM organization_members WHERE user_id = auth.uid()));
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_org_members_user ON organization_members(user_id);
|
||||
CREATE INDEX idx_org_members_org ON organization_members(organization_id);
|
||||
CREATE INDEX idx_projects_org ON projects(organization_id);
|
||||
```
|
||||
|
||||
### TypeScript Service Layer
|
||||
|
||||
```typescript
|
||||
// services/organization-service.ts
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import type { Database } from '@/types/database'
|
||||
|
||||
type Organization = Database['public']['Tables']['organizations']['Row']
|
||||
type OrganizationMember = Database['public']['Tables']['organization_members']['Row']
|
||||
|
||||
export class OrganizationService {
|
||||
async create(name: string, slug: string): Promise<Organization> {
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) throw new Error('Not authenticated')
|
||||
|
||||
// Create organization
|
||||
const { data: org, error: orgError } = await supabase
|
||||
.from('organizations')
|
||||
.insert({ name, slug })
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (orgError) throw orgError
|
||||
|
||||
// Add creator as owner
|
||||
const { error: memberError } = await supabase
|
||||
.from('organization_members')
|
||||
.insert({
|
||||
organization_id: org.id,
|
||||
user_id: user.id,
|
||||
role: 'owner'
|
||||
})
|
||||
|
||||
if (memberError) {
|
||||
// Rollback org creation
|
||||
await supabase.from('organizations').delete().eq('id', org.id)
|
||||
throw memberError
|
||||
}
|
||||
|
||||
return org
|
||||
}
|
||||
|
||||
async getMyOrganizations(): Promise<Organization[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('organizations')
|
||||
.select('*, organization_members!inner(role)')
|
||||
.order('name')
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
async getMembers(orgId: string): Promise<OrganizationMember[]> {
|
||||
const { data, error } = await supabase
|
||||
.from('organization_members')
|
||||
.select('*, user:profiles(*)')
|
||||
.eq('organization_id', orgId)
|
||||
.order('joined_at')
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
async inviteMember(orgId: string, email: string, role: string): Promise<void> {
|
||||
const { error } = await supabase.functions.invoke('invite-member', {
|
||||
body: { organizationId: orgId, email, role }
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const organizationService = new OrganizationService()
|
||||
```
|
||||
|
||||
## AI Document Search Application
|
||||
|
||||
### Database Schema
|
||||
|
||||
```sql
|
||||
-- Enable extensions
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
-- Documents with embeddings
|
||||
CREATE TABLE documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
embedding vector(1536),
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_by UUID NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- HNSW index for fast similarity search
|
||||
CREATE INDEX idx_documents_embedding ON documents
|
||||
USING hnsw (embedding vector_cosine_ops)
|
||||
WITH (m = 16, ef_construction = 64);
|
||||
|
||||
-- Full-text search index
|
||||
CREATE INDEX idx_documents_content_fts ON documents
|
||||
USING gin(to_tsvector('english', content));
|
||||
|
||||
-- Enable RLS
|
||||
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "document_access" ON documents FOR ALL
|
||||
USING (project_id IN (
|
||||
SELECT p.id FROM projects p
|
||||
JOIN organization_members om ON p.organization_id = om.organization_id
|
||||
WHERE om.user_id = auth.uid()
|
||||
));
|
||||
|
||||
-- Semantic search function
|
||||
CREATE OR REPLACE FUNCTION search_documents(
|
||||
p_project_id UUID,
|
||||
p_query_embedding vector(1536),
|
||||
p_match_threshold FLOAT DEFAULT 0.7,
|
||||
p_match_count INT DEFAULT 10
|
||||
) RETURNS TABLE (
|
||||
id UUID,
|
||||
title TEXT,
|
||||
content TEXT,
|
||||
similarity FLOAT
|
||||
) LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT d.id, d.title, d.content,
|
||||
1 - (d.embedding <=> p_query_embedding) AS similarity
|
||||
FROM documents d
|
||||
WHERE d.project_id = p_project_id
|
||||
AND 1 - (d.embedding <=> p_query_embedding) > p_match_threshold
|
||||
ORDER BY d.embedding <=> p_query_embedding
|
||||
LIMIT p_match_count;
|
||||
END; $$;
|
||||
```
|
||||
|
||||
### Edge Function for Embeddings
|
||||
|
||||
```typescript
|
||||
// supabase/functions/generate-embedding/index.ts
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
|
||||
try {
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
||||
)
|
||||
|
||||
const { documentId, content } = await req.json()
|
||||
|
||||
// Generate embedding using OpenAI
|
||||
const embeddingResponse = await fetch('https://api.openai.com/v1/embeddings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'text-embedding-ada-002',
|
||||
input: content.slice(0, 8000)
|
||||
})
|
||||
})
|
||||
|
||||
const embeddingData = await embeddingResponse.json()
|
||||
const embedding = embeddingData.data[0].embedding
|
||||
|
||||
// Update document with embedding
|
||||
const { error } = await supabase
|
||||
.from('documents')
|
||||
.update({ embedding })
|
||||
.eq('id', documentId)
|
||||
|
||||
if (error) throw error
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true }),
|
||||
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
)
|
||||
} catch (error) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: error.message }),
|
||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### React Search Component
|
||||
|
||||
```typescript
|
||||
// components/DocumentSearch.tsx
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useDebounce } from '@/hooks/useDebounce'
|
||||
import { documentService } from '@/services/document-service'
|
||||
|
||||
interface SearchResult {
|
||||
id: string
|
||||
title: string
|
||||
content: string
|
||||
similarity: number
|
||||
}
|
||||
|
||||
export function DocumentSearch({ projectId }: { projectId: string }) {
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState<SearchResult[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const debouncedQuery = useDebounce(query, 300)
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedQuery.length < 3) {
|
||||
setResults([])
|
||||
return
|
||||
}
|
||||
|
||||
async function search() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await documentService.semanticSearch(projectId, debouncedQuery)
|
||||
setResults(data)
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
search()
|
||||
}, [debouncedQuery, projectId])
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search documents..."
|
||||
className="w-full px-4 py-2 border rounded-lg"
|
||||
/>
|
||||
|
||||
{loading && <div>Searching...</div>}
|
||||
|
||||
<div className="space-y-2">
|
||||
{results.map((result) => (
|
||||
<div key={result.id} className="p-4 border rounded-lg">
|
||||
<div className="flex justify-between">
|
||||
<h3 className="font-medium">{result.title}</h3>
|
||||
<span className="text-sm text-gray-500">
|
||||
{(result.similarity * 100).toFixed(1)}% match
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-gray-600 line-clamp-2">{result.content}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Real-Time Collaboration
|
||||
|
||||
### Collaborative Editor with Presence
|
||||
|
||||
```typescript
|
||||
// components/CollaborativeEditor.tsx
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
name: string
|
||||
color: string
|
||||
}
|
||||
|
||||
interface PresenceState {
|
||||
user: User
|
||||
cursor: { x: number; y: number } | null
|
||||
selection: { start: number; end: number } | null
|
||||
}
|
||||
|
||||
export function CollaborativeEditor({
|
||||
documentId,
|
||||
currentUser
|
||||
}: {
|
||||
documentId: string
|
||||
currentUser: User
|
||||
}) {
|
||||
const [content, setContent] = useState('')
|
||||
const [otherUsers, setOtherUsers] = useState<PresenceState[]>([])
|
||||
const [channel, setChannel] = useState<ReturnType<typeof supabase.channel> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const ch = supabase.channel(`doc:${documentId}`, {
|
||||
config: { presence: { key: currentUser.id } }
|
||||
})
|
||||
|
||||
ch.on('presence', { event: 'sync' }, () => {
|
||||
const state = ch.presenceState<PresenceState>()
|
||||
const users = Object.values(state)
|
||||
.flat()
|
||||
.filter((p) => p.user.id !== currentUser.id)
|
||||
setOtherUsers(users)
|
||||
})
|
||||
|
||||
ch.on('broadcast', { event: 'content-update' }, ({ payload }) => {
|
||||
if (payload.userId !== currentUser.id) {
|
||||
setContent(payload.content)
|
||||
}
|
||||
})
|
||||
|
||||
ch.subscribe(async (status) => {
|
||||
if (status === 'SUBSCRIBED') {
|
||||
await ch.track({
|
||||
user: currentUser,
|
||||
cursor: null,
|
||||
selection: null
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
setChannel(ch)
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(ch)
|
||||
}
|
||||
}, [documentId, currentUser])
|
||||
|
||||
const handleContentChange = useCallback(
|
||||
async (newContent: string) => {
|
||||
setContent(newContent)
|
||||
if (channel) {
|
||||
await channel.send({
|
||||
type: 'broadcast',
|
||||
event: 'content-update',
|
||||
payload: { userId: currentUser.id, content: newContent }
|
||||
})
|
||||
}
|
||||
},
|
||||
[channel, currentUser.id]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={content}
|
||||
onChange={(e) => handleContentChange(e.target.value)}
|
||||
className="w-full h-96 p-4 border rounded-lg"
|
||||
/>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<span className="text-sm text-gray-500">Active:</span>
|
||||
{otherUsers.map((presence) => (
|
||||
<span
|
||||
key={presence.user.id}
|
||||
className="px-2 py-1 text-xs rounded-full"
|
||||
style={{ backgroundColor: presence.user.color + '20', color: presence.user.color }}
|
||||
>
|
||||
{presence.user.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Project Structure Template
|
||||
|
||||
```
|
||||
my-supabase-app/
|
||||
├── supabase/
|
||||
│ ├── functions/
|
||||
│ │ ├── generate-embedding/
|
||||
│ │ │ └── index.ts
|
||||
│ │ └── invite-member/
|
||||
│ │ └── index.ts
|
||||
│ ├── migrations/
|
||||
│ │ ├── 20240101000000_initial_schema.sql
|
||||
│ │ └── 20240101000001_add_embeddings.sql
|
||||
│ └── config.toml
|
||||
├── src/
|
||||
│ ├── lib/
|
||||
│ │ └── supabase/
|
||||
│ │ ├── client.ts
|
||||
│ │ └── server.ts
|
||||
│ ├── services/
|
||||
│ │ ├── organization-service.ts
|
||||
│ │ ├── project-service.ts
|
||||
│ │ └── document-service.ts
|
||||
│ ├── types/
|
||||
│ │ └── database.ts
|
||||
│ └── components/
|
||||
│ ├── DocumentSearch.tsx
|
||||
│ ├── CollaborativeEditor.tsx
|
||||
│ └── FileUploader.tsx
|
||||
├── .env.local
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
```bash
|
||||
# .env.local
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
|
||||
# Server-side only
|
||||
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
|
||||
# Edge Functions secrets (set via CLI)
|
||||
# supabase secrets set OPENAI_API_KEY=sk-...
|
||||
```
|
||||
|
|
@ -0,0 +1,384 @@
|
|||
---
|
||||
name: auth-integration
|
||||
description: Authentication patterns and JWT handling for Supabase applications
|
||||
parent-skill: moai-platform-supabase
|
||||
version: 1.0.0
|
||||
updated: 2026-01-06
|
||||
---
|
||||
|
||||
# Auth Integration Module
|
||||
|
||||
## Overview
|
||||
|
||||
Supabase Auth provides authentication with multiple providers, JWT-based sessions, and seamless integration with Row-Level Security policies.
|
||||
|
||||
## Client Setup
|
||||
|
||||
### Browser Client
|
||||
|
||||
```typescript
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
)
|
||||
```
|
||||
|
||||
### Server-Side Client (Next.js App Router)
|
||||
|
||||
```typescript
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
import { Database } from './database.types'
|
||||
|
||||
export function createServerSupabase() {
|
||||
const cookieStore = cookies()
|
||||
|
||||
return createServerClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return cookieStore.get(name)?.value
|
||||
},
|
||||
set(name, value, options) {
|
||||
cookieStore.set({ name, value, ...options })
|
||||
},
|
||||
remove(name, options) {
|
||||
cookieStore.set({ name, value: '', ...options })
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware Client (Next.js)
|
||||
|
||||
```typescript
|
||||
// middleware.ts
|
||||
import { createServerClient, type CookieOptions } from '@supabase/ssr'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
let response = NextResponse.next({
|
||||
request: { headers: request.headers }
|
||||
})
|
||||
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return request.cookies.get(name)?.value
|
||||
},
|
||||
set(name: string, value: string, options: CookieOptions) {
|
||||
request.cookies.set({ name, value, ...options })
|
||||
response.cookies.set({ name, value, ...options })
|
||||
},
|
||||
remove(name: string, options: CookieOptions) {
|
||||
request.cookies.set({ name, value: '', ...options })
|
||||
response.cookies.set({ name, value: '', ...options })
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await supabase.auth.getUser()
|
||||
return response
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
### Email/Password Sign Up
|
||||
|
||||
```typescript
|
||||
async function signUp(email: string, password: string) {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
emailRedirectTo: `${window.location.origin}/auth/callback`
|
||||
}
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
### Email/Password Sign In
|
||||
|
||||
```typescript
|
||||
async function signIn(email: string, password: string) {
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
### OAuth Provider
|
||||
|
||||
```typescript
|
||||
async function signInWithGoogle() {
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'google',
|
||||
options: {
|
||||
redirectTo: `${window.location.origin}/auth/callback`,
|
||||
queryParams: {
|
||||
access_type: 'offline',
|
||||
prompt: 'consent'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
### Magic Link
|
||||
|
||||
```typescript
|
||||
async function signInWithMagicLink(email: string) {
|
||||
const { data, error } = await supabase.auth.signInWithOtp({
|
||||
email,
|
||||
options: {
|
||||
emailRedirectTo: `${window.location.origin}/auth/callback`
|
||||
}
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
## Session Management
|
||||
|
||||
### Get Current User
|
||||
|
||||
```typescript
|
||||
async function getCurrentUser() {
|
||||
const { data: { user }, error } = await supabase.auth.getUser()
|
||||
if (error) throw error
|
||||
return user
|
||||
}
|
||||
```
|
||||
|
||||
### Get Session
|
||||
|
||||
```typescript
|
||||
async function getSession() {
|
||||
const { data: { session }, error } = await supabase.auth.getSession()
|
||||
if (error) throw error
|
||||
return session
|
||||
}
|
||||
```
|
||||
|
||||
### Sign Out
|
||||
|
||||
```typescript
|
||||
async function signOut() {
|
||||
const { error } = await supabase.auth.signOut()
|
||||
if (error) throw error
|
||||
}
|
||||
```
|
||||
|
||||
### Listen to Auth Changes
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
||||
(event, session) => {
|
||||
if (event === 'SIGNED_IN') {
|
||||
console.log('User signed in:', session?.user)
|
||||
}
|
||||
if (event === 'SIGNED_OUT') {
|
||||
console.log('User signed out')
|
||||
}
|
||||
if (event === 'TOKEN_REFRESHED') {
|
||||
console.log('Token refreshed')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}, [])
|
||||
```
|
||||
|
||||
## Auth Callback Handler
|
||||
|
||||
### Next.js App Router
|
||||
|
||||
```typescript
|
||||
// app/auth/callback/route.ts
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams, origin } = new URL(request.url)
|
||||
const code = searchParams.get('code')
|
||||
const next = searchParams.get('next') ?? '/'
|
||||
|
||||
if (code) {
|
||||
const cookieStore = cookies()
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return cookieStore.get(name)?.value
|
||||
},
|
||||
set(name, value, options) {
|
||||
cookieStore.set({ name, value, ...options })
|
||||
},
|
||||
remove(name, options) {
|
||||
cookieStore.set({ name, value: '', ...options })
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const { error } = await supabase.auth.exchangeCodeForSession(code)
|
||||
if (!error) {
|
||||
return NextResponse.redirect(`${origin}${next}`)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.redirect(`${origin}/auth/error`)
|
||||
}
|
||||
```
|
||||
|
||||
## Protected Routes
|
||||
|
||||
### Server Component Protection
|
||||
|
||||
```typescript
|
||||
// app/dashboard/page.tsx
|
||||
import { redirect } from 'next/navigation'
|
||||
import { createServerSupabase } from '@/lib/supabase/server'
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const supabase = createServerSupabase()
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
return <Dashboard user={user} />
|
||||
}
|
||||
```
|
||||
|
||||
### Client Component Protection
|
||||
|
||||
```typescript
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
export function useRequireAuth() {
|
||||
const [user, setUser] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getUser().then(({ data: { user } }) => {
|
||||
if (!user) {
|
||||
router.push('/login')
|
||||
} else {
|
||||
setUser(user)
|
||||
}
|
||||
setLoading(false)
|
||||
})
|
||||
}, [router])
|
||||
|
||||
return { user, loading }
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Claims
|
||||
|
||||
### Setting Custom Claims (Edge Function)
|
||||
|
||||
```typescript
|
||||
// supabase/functions/set-claims/index.ts
|
||||
serve(async (req) => {
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
||||
)
|
||||
|
||||
const { userId, claims } = await req.json()
|
||||
|
||||
// Update user metadata (available in JWT)
|
||||
const { error } = await supabase.auth.admin.updateUserById(userId, {
|
||||
app_metadata: claims
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
|
||||
return new Response(JSON.stringify({ success: true }))
|
||||
})
|
||||
```
|
||||
|
||||
### Reading Claims in RLS
|
||||
|
||||
```sql
|
||||
-- Access claims in RLS policies
|
||||
CREATE POLICY "admin_only" ON admin_data FOR ALL
|
||||
USING ((auth.jwt() ->> 'role')::text = 'admin');
|
||||
```
|
||||
|
||||
## Password Reset
|
||||
|
||||
```typescript
|
||||
async function resetPassword(email: string) {
|
||||
const { data, error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${window.location.origin}/auth/reset-password`
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
async function updatePassword(newPassword: string) {
|
||||
const { data, error } = await supabase.auth.updateUser({
|
||||
password: newPassword
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
## Context7 Query Examples
|
||||
|
||||
For latest Auth documentation:
|
||||
|
||||
Topic: "supabase auth signIn signUp"
|
||||
Topic: "supabase ssr server client"
|
||||
Topic: "auth jwt claims custom"
|
||||
|
||||
---
|
||||
|
||||
Related Modules:
|
||||
- row-level-security.md - Auth integration with RLS
|
||||
- typescript-patterns.md - Type-safe auth patterns
|
||||
- edge-functions.md - Server-side auth verification
|
||||
371
.claude/skills/moai-platform-supabase/modules/edge-functions.md
Normal file
371
.claude/skills/moai-platform-supabase/modules/edge-functions.md
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
---
|
||||
name: edge-functions
|
||||
description: Serverless Deno functions at the edge with authentication and rate limiting
|
||||
parent-skill: moai-platform-supabase
|
||||
version: 1.0.0
|
||||
updated: 2026-01-06
|
||||
---
|
||||
|
||||
# Edge Functions Module
|
||||
|
||||
## Overview
|
||||
|
||||
Supabase Edge Functions are serverless functions running on the Deno runtime at the edge, providing low-latency responses globally.
|
||||
|
||||
## Basic Edge Function
|
||||
|
||||
### Function Structure
|
||||
|
||||
```typescript
|
||||
// supabase/functions/api/index.ts
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
// Handle CORS preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
|
||||
try {
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
||||
)
|
||||
|
||||
// Process request
|
||||
const body = await req.json()
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, data: body }),
|
||||
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
)
|
||||
} catch (error) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: error.message }),
|
||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
### JWT Token Verification
|
||||
|
||||
```typescript
|
||||
serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers: corsHeaders })
|
||||
}
|
||||
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
||||
)
|
||||
|
||||
// Verify JWT token
|
||||
const authHeader = req.headers.get('authorization')
|
||||
if (!authHeader) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Unauthorized' }),
|
||||
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
|
||||
const { data: { user }, error } = await supabase.auth.getUser(
|
||||
authHeader.replace('Bearer ', '')
|
||||
)
|
||||
|
||||
if (error || !user) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Invalid token' }),
|
||||
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
|
||||
// User is authenticated, proceed with request
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, user_id: user.id }),
|
||||
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
### Using User Context Client
|
||||
|
||||
Create a client that inherits user permissions:
|
||||
|
||||
```typescript
|
||||
serve(async (req) => {
|
||||
const authHeader = req.headers.get('authorization')!
|
||||
|
||||
// Client with user's permissions (respects RLS)
|
||||
const supabaseUser = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_ANON_KEY')!,
|
||||
{ global: { headers: { Authorization: authHeader } } }
|
||||
)
|
||||
|
||||
// This query respects RLS policies
|
||||
const { data, error } = await supabaseUser
|
||||
.from('projects')
|
||||
.select('*')
|
||||
|
||||
return new Response(JSON.stringify({ data }))
|
||||
})
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Database-Based Rate Limiting
|
||||
|
||||
```sql
|
||||
-- Rate limits table
|
||||
CREATE TABLE rate_limits (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
identifier TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_rate_limits_lookup ON rate_limits(identifier, created_at);
|
||||
```
|
||||
|
||||
### Rate Limit Function
|
||||
|
||||
```typescript
|
||||
async function checkRateLimit(
|
||||
supabase: SupabaseClient,
|
||||
identifier: string,
|
||||
limit: number,
|
||||
windowSeconds: number
|
||||
): Promise<boolean> {
|
||||
const windowStart = new Date(Date.now() - windowSeconds * 1000).toISOString()
|
||||
|
||||
const { count } = await supabase
|
||||
.from('rate_limits')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('identifier', identifier)
|
||||
.gte('created_at', windowStart)
|
||||
|
||||
if (count && count >= limit) {
|
||||
return false
|
||||
}
|
||||
|
||||
await supabase.from('rate_limits').insert({ identifier })
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
### Usage in Edge Function
|
||||
|
||||
```typescript
|
||||
serve(async (req) => {
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
||||
)
|
||||
|
||||
// Get client identifier (IP or user ID)
|
||||
const identifier = req.headers.get('x-forwarded-for') || 'anonymous'
|
||||
|
||||
// 100 requests per minute
|
||||
const allowed = await checkRateLimit(supabase, identifier, 100, 60)
|
||||
|
||||
if (!allowed) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Rate limit exceeded' }),
|
||||
{ status: 429, headers: corsHeaders }
|
||||
)
|
||||
}
|
||||
|
||||
// Process request...
|
||||
})
|
||||
```
|
||||
|
||||
## External API Integration
|
||||
|
||||
### Webhook Handler
|
||||
|
||||
```typescript
|
||||
serve(async (req) => {
|
||||
const supabase = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
||||
)
|
||||
|
||||
// Verify webhook signature
|
||||
const signature = req.headers.get('x-webhook-signature')
|
||||
const body = await req.text()
|
||||
|
||||
const expectedSignature = await crypto.subtle.digest(
|
||||
'SHA-256',
|
||||
new TextEncoder().encode(body + Deno.env.get('WEBHOOK_SECRET'))
|
||||
)
|
||||
|
||||
if (!verifySignature(signature, expectedSignature)) {
|
||||
return new Response('Invalid signature', { status: 401 })
|
||||
}
|
||||
|
||||
const payload = JSON.parse(body)
|
||||
|
||||
// Process webhook
|
||||
await supabase.from('webhook_events').insert({
|
||||
type: payload.type,
|
||||
data: payload.data,
|
||||
processed: false
|
||||
})
|
||||
|
||||
return new Response('OK', { status: 200 })
|
||||
})
|
||||
```
|
||||
|
||||
### External API Call
|
||||
|
||||
```typescript
|
||||
serve(async (req) => {
|
||||
const { query } = await req.json()
|
||||
|
||||
// Call external API
|
||||
const response = await fetch('https://api.openai.com/v1/embeddings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'text-embedding-ada-002',
|
||||
input: query
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ embedding: data.data[0].embedding }),
|
||||
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Structured Error Response
|
||||
|
||||
```typescript
|
||||
interface ErrorResponse {
|
||||
error: string
|
||||
code: string
|
||||
details?: unknown
|
||||
}
|
||||
|
||||
function errorResponse(
|
||||
message: string,
|
||||
code: string,
|
||||
status: number,
|
||||
details?: unknown
|
||||
): Response {
|
||||
const body: ErrorResponse = { error: message, code, details }
|
||||
return new Response(
|
||||
JSON.stringify(body),
|
||||
{ status, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||
)
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
try {
|
||||
// ... processing
|
||||
} catch (error) {
|
||||
if (error instanceof AuthError) {
|
||||
return errorResponse('Authentication failed', 'AUTH_ERROR', 401)
|
||||
}
|
||||
if (error instanceof ValidationError) {
|
||||
return errorResponse('Invalid input', 'VALIDATION_ERROR', 400, error.details)
|
||||
}
|
||||
console.error('Unexpected error:', error)
|
||||
return errorResponse('Internal server error', 'INTERNAL_ERROR', 500)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
supabase functions serve api --env-file .env.local
|
||||
```
|
||||
|
||||
### Deploy Function
|
||||
|
||||
```bash
|
||||
supabase functions deploy api
|
||||
```
|
||||
|
||||
### Set Secrets
|
||||
|
||||
```bash
|
||||
supabase secrets set OPENAI_API_KEY=sk-xxx
|
||||
supabase secrets set WEBHOOK_SECRET=whsec-xxx
|
||||
```
|
||||
|
||||
### List Functions
|
||||
|
||||
```bash
|
||||
supabase functions list
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Cold Start Optimization
|
||||
|
||||
Keep imports minimal at the top level:
|
||||
|
||||
```typescript
|
||||
// Good: Import only what's needed
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||
|
||||
// Bad: Heavy imports at top level increase cold start
|
||||
// import { everything } from 'large-library'
|
||||
```
|
||||
|
||||
### Response Streaming
|
||||
|
||||
Stream large responses:
|
||||
|
||||
```typescript
|
||||
serve(async (req) => {
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise(r => setTimeout(r, 100))
|
||||
controller.enqueue(new TextEncoder().encode(`data: ${i}\n\n`))
|
||||
}
|
||||
controller.close()
|
||||
}
|
||||
})
|
||||
|
||||
return new Response(stream, {
|
||||
headers: { 'Content-Type': 'text/event-stream' }
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Context7 Query Examples
|
||||
|
||||
For latest Edge Functions documentation:
|
||||
|
||||
Topic: "edge functions deno runtime"
|
||||
Topic: "supabase functions deploy secrets"
|
||||
Topic: "edge functions cors authentication"
|
||||
|
||||
---
|
||||
|
||||
Related Modules:
|
||||
- auth-integration.md - Authentication patterns
|
||||
- typescript-patterns.md - Client invocation
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
---
|
||||
name: postgresql-pgvector
|
||||
description: PostgreSQL 16 with pgvector extension for AI embeddings and semantic search
|
||||
parent-skill: moai-platform-supabase
|
||||
version: 1.0.0
|
||||
updated: 2026-01-06
|
||||
---
|
||||
|
||||
# PostgreSQL 16 + pgvector Module
|
||||
|
||||
## Overview
|
||||
|
||||
PostgreSQL 16 with pgvector extension enables AI-powered semantic search through vector embeddings storage and similarity search operations.
|
||||
|
||||
## Extension Setup
|
||||
|
||||
Enable required extensions for vector operations:
|
||||
|
||||
```sql
|
||||
-- Enable required extensions
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
```
|
||||
|
||||
## Embeddings Table Schema
|
||||
|
||||
Create a table optimized for storing AI embeddings:
|
||||
|
||||
```sql
|
||||
CREATE TABLE documents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
content TEXT NOT NULL,
|
||||
embedding vector(1536), -- OpenAI ada-002 dimensions
|
||||
metadata JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### Common Embedding Dimensions
|
||||
|
||||
- OpenAI ada-002: 1536 dimensions
|
||||
- OpenAI text-embedding-3-small: 1536 dimensions
|
||||
- OpenAI text-embedding-3-large: 3072 dimensions
|
||||
- Cohere embed-english-v3.0: 1024 dimensions
|
||||
- Google PaLM: 768 dimensions
|
||||
|
||||
## Index Strategies
|
||||
|
||||
### HNSW Index (Recommended)
|
||||
|
||||
HNSW (Hierarchical Navigable Small World) provides fast approximate nearest neighbor search:
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_documents_embedding ON documents
|
||||
USING hnsw (embedding vector_cosine_ops)
|
||||
WITH (m = 16, ef_construction = 64);
|
||||
```
|
||||
|
||||
Parameters:
|
||||
- m: Maximum number of connections per layer (default 16, higher = more accurate but slower)
|
||||
- ef_construction: Size of dynamic candidate list during construction (default 64)
|
||||
|
||||
### IVFFlat Index (Large Datasets)
|
||||
|
||||
IVFFlat is better for datasets with millions of rows:
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_documents_ivf ON documents
|
||||
USING ivfflat (embedding vector_cosine_ops)
|
||||
WITH (lists = 100);
|
||||
```
|
||||
|
||||
Guidelines for lists parameter:
|
||||
- Less than 1M rows: lists = rows / 1000
|
||||
- More than 1M rows: lists = sqrt(rows)
|
||||
|
||||
## Distance Operations
|
||||
|
||||
Available distance operators:
|
||||
|
||||
- `<->` - Euclidean distance (L2)
|
||||
- `<#>` - Negative inner product
|
||||
- `<=>` - Cosine distance
|
||||
|
||||
For normalized embeddings, cosine distance is recommended.
|
||||
|
||||
## Semantic Search Function
|
||||
|
||||
Basic semantic search with threshold and limit:
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION search_documents(
|
||||
query_embedding vector(1536),
|
||||
match_threshold FLOAT DEFAULT 0.8,
|
||||
match_count INT DEFAULT 10
|
||||
) RETURNS TABLE (id UUID, content TEXT, similarity FLOAT)
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
RETURN QUERY SELECT d.id, d.content,
|
||||
1 - (d.embedding <=> query_embedding) AS similarity
|
||||
FROM documents d
|
||||
WHERE 1 - (d.embedding <=> query_embedding) > match_threshold
|
||||
ORDER BY d.embedding <=> query_embedding
|
||||
LIMIT match_count;
|
||||
END; $$;
|
||||
```
|
||||
|
||||
### Usage Example
|
||||
|
||||
```sql
|
||||
SELECT * FROM search_documents(
|
||||
'[0.1, 0.2, ...]'::vector(1536),
|
||||
0.75,
|
||||
20
|
||||
);
|
||||
```
|
||||
|
||||
## Hybrid Search
|
||||
|
||||
Combine vector similarity with full-text search for better results:
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION hybrid_search(
|
||||
query_text TEXT,
|
||||
query_embedding vector(1536),
|
||||
match_count INT DEFAULT 10,
|
||||
full_text_weight FLOAT DEFAULT 0.3,
|
||||
semantic_weight FLOAT DEFAULT 0.7
|
||||
) RETURNS TABLE (id UUID, content TEXT, score FLOAT) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH semantic AS (
|
||||
SELECT e.id, e.content, 1 - (e.embedding <=> query_embedding) AS similarity
|
||||
FROM documents e ORDER BY e.embedding <=> query_embedding LIMIT match_count * 2
|
||||
),
|
||||
full_text AS (
|
||||
SELECT e.id, e.content,
|
||||
ts_rank(to_tsvector('english', e.content), plainto_tsquery('english', query_text)) AS rank
|
||||
FROM documents e
|
||||
WHERE to_tsvector('english', e.content) @@ plainto_tsquery('english', query_text)
|
||||
LIMIT match_count * 2
|
||||
)
|
||||
SELECT COALESCE(s.id, f.id), COALESCE(s.content, f.content),
|
||||
(COALESCE(s.similarity, 0) * semantic_weight + COALESCE(f.rank, 0) * full_text_weight)
|
||||
FROM semantic s FULL OUTER JOIN full_text f ON s.id = f.id
|
||||
ORDER BY 3 DESC LIMIT match_count;
|
||||
END; $$ LANGUAGE plpgsql;
|
||||
```
|
||||
|
||||
## Full-Text Search Index
|
||||
|
||||
Add GIN index for efficient full-text search:
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_documents_content_fts ON documents
|
||||
USING gin(to_tsvector('english', content));
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Query Performance
|
||||
|
||||
Set appropriate ef_search for HNSW queries:
|
||||
|
||||
```sql
|
||||
SET hnsw.ef_search = 100; -- Higher = more accurate, slower
|
||||
```
|
||||
|
||||
### Batch Insertions
|
||||
|
||||
Use COPY or multi-row INSERT for bulk embeddings:
|
||||
|
||||
```sql
|
||||
INSERT INTO documents (content, embedding, metadata)
|
||||
VALUES
|
||||
('Content 1', '[...]'::vector(1536), '{"source": "doc1"}'),
|
||||
('Content 2', '[...]'::vector(1536), '{"source": "doc2"}'),
|
||||
('Content 3', '[...]'::vector(1536), '{"source": "doc3"}');
|
||||
```
|
||||
|
||||
### Index Maintenance
|
||||
|
||||
Reindex after large bulk insertions:
|
||||
|
||||
```sql
|
||||
REINDEX INDEX CONCURRENTLY idx_documents_embedding;
|
||||
```
|
||||
|
||||
## Metadata Filtering
|
||||
|
||||
Combine vector search with JSONB metadata filters:
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION search_with_filters(
|
||||
query_embedding vector(1536),
|
||||
filter_metadata JSONB,
|
||||
match_count INT DEFAULT 10
|
||||
) RETURNS TABLE (id UUID, content TEXT, similarity FLOAT) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY SELECT d.id, d.content,
|
||||
1 - (d.embedding <=> query_embedding) AS similarity
|
||||
FROM documents d
|
||||
WHERE d.metadata @> filter_metadata
|
||||
ORDER BY d.embedding <=> query_embedding
|
||||
LIMIT match_count;
|
||||
END; $$;
|
||||
```
|
||||
|
||||
### Usage with Filters
|
||||
|
||||
```sql
|
||||
SELECT * FROM search_with_filters(
|
||||
'[0.1, 0.2, ...]'::vector(1536),
|
||||
'{"category": "technical", "language": "en"}'::jsonb,
|
||||
10
|
||||
);
|
||||
```
|
||||
|
||||
## Context7 Query Examples
|
||||
|
||||
For latest pgvector documentation:
|
||||
|
||||
Topic: "pgvector extension indexes hnsw ivfflat"
|
||||
Topic: "vector similarity search operators"
|
||||
Topic: "postgresql full-text search tsvector"
|
||||
|
||||
---
|
||||
|
||||
Related Modules:
|
||||
- row-level-security.md - Secure vector data access
|
||||
- typescript-patterns.md - Client-side search implementation
|
||||
|
|
@ -0,0 +1,354 @@
|
|||
---
|
||||
name: realtime-presence
|
||||
description: Real-time subscriptions and presence tracking for collaborative features
|
||||
parent-skill: moai-platform-supabase
|
||||
version: 1.0.0
|
||||
updated: 2026-01-06
|
||||
---
|
||||
|
||||
# Real-time and Presence Module
|
||||
|
||||
## Overview
|
||||
|
||||
Supabase provides real-time capabilities through Postgres Changes (database change notifications) and Presence (user state tracking) for building collaborative applications.
|
||||
|
||||
## Postgres Changes Subscription
|
||||
|
||||
### Basic Setup
|
||||
|
||||
Subscribe to all changes on a table:
|
||||
|
||||
```typescript
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
|
||||
|
||||
const channel = supabase.channel('db-changes')
|
||||
.on('postgres_changes',
|
||||
{ event: '*', schema: 'public', table: 'messages' },
|
||||
(payload) => console.log('Change:', payload)
|
||||
)
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
### Event Types
|
||||
|
||||
Available events:
|
||||
- `INSERT` - New row added
|
||||
- `UPDATE` - Row modified
|
||||
- `DELETE` - Row removed
|
||||
- `*` - All events
|
||||
|
||||
### Filtered Subscriptions
|
||||
|
||||
Filter changes by specific conditions:
|
||||
|
||||
```typescript
|
||||
supabase.channel('project-updates')
|
||||
.on('postgres_changes',
|
||||
{
|
||||
event: 'UPDATE',
|
||||
schema: 'public',
|
||||
table: 'projects',
|
||||
filter: `id=eq.${projectId}`
|
||||
},
|
||||
(payload) => handleProjectUpdate(payload.new)
|
||||
)
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
### Multiple Tables
|
||||
|
||||
Subscribe to multiple tables on one channel:
|
||||
|
||||
```typescript
|
||||
const channel = supabase.channel('app-changes')
|
||||
.on('postgres_changes',
|
||||
{ event: '*', schema: 'public', table: 'tasks' },
|
||||
handleTaskChange
|
||||
)
|
||||
.on('postgres_changes',
|
||||
{ event: '*', schema: 'public', table: 'comments' },
|
||||
handleCommentChange
|
||||
)
|
||||
.subscribe()
|
||||
```
|
||||
|
||||
## Presence Tracking
|
||||
|
||||
### Presence State Interface
|
||||
|
||||
```typescript
|
||||
interface PresenceState {
|
||||
user_id: string
|
||||
online_at: string
|
||||
typing?: boolean
|
||||
cursor?: { x: number; y: number }
|
||||
}
|
||||
```
|
||||
|
||||
### Channel Setup with Presence
|
||||
|
||||
```typescript
|
||||
const channel = supabase.channel('room:collaborative-doc', {
|
||||
config: { presence: { key: userId } }
|
||||
})
|
||||
|
||||
channel
|
||||
.on('presence', { event: 'sync' }, () => {
|
||||
const state = channel.presenceState<PresenceState>()
|
||||
console.log('Online users:', Object.keys(state))
|
||||
})
|
||||
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
|
||||
console.log('User joined:', key, newPresences)
|
||||
})
|
||||
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
|
||||
console.log('User left:', key, leftPresences)
|
||||
})
|
||||
.subscribe(async (status) => {
|
||||
if (status === 'SUBSCRIBED') {
|
||||
await channel.track({
|
||||
user_id: userId,
|
||||
online_at: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Update Presence State
|
||||
|
||||
Update user presence in real-time:
|
||||
|
||||
```typescript
|
||||
// Track typing status
|
||||
await channel.track({ typing: true })
|
||||
|
||||
// Track cursor position
|
||||
await channel.track({ cursor: { x: 100, y: 200 } })
|
||||
|
||||
// Clear typing after timeout
|
||||
setTimeout(async () => {
|
||||
await channel.track({ typing: false })
|
||||
}, 1000)
|
||||
```
|
||||
|
||||
## Collaborative Features
|
||||
|
||||
### Collaborative Cursors
|
||||
|
||||
```typescript
|
||||
interface CursorState {
|
||||
user_id: string
|
||||
user_name: string
|
||||
cursor: { x: number; y: number }
|
||||
color: string
|
||||
}
|
||||
|
||||
function setupCollaborativeCursors(documentId: string, userId: string, userName: string) {
|
||||
const channel = supabase.channel(`cursors:${documentId}`, {
|
||||
config: { presence: { key: userId } }
|
||||
})
|
||||
|
||||
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7']
|
||||
const userColor = colors[Math.abs(userId.hashCode()) % colors.length]
|
||||
|
||||
channel
|
||||
.on('presence', { event: 'sync' }, () => {
|
||||
const state = channel.presenceState<CursorState>()
|
||||
renderCursors(Object.values(state).flat())
|
||||
})
|
||||
.subscribe(async (status) => {
|
||||
if (status === 'SUBSCRIBED') {
|
||||
await channel.track({
|
||||
user_id: userId,
|
||||
user_name: userName,
|
||||
cursor: { x: 0, y: 0 },
|
||||
color: userColor
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Track mouse movement
|
||||
document.addEventListener('mousemove', async (e) => {
|
||||
await channel.track({
|
||||
user_id: userId,
|
||||
user_name: userName,
|
||||
cursor: { x: e.clientX, y: e.clientY },
|
||||
color: userColor
|
||||
})
|
||||
})
|
||||
|
||||
return channel
|
||||
}
|
||||
```
|
||||
|
||||
### Live Editing Indicators
|
||||
|
||||
```typescript
|
||||
interface EditingState {
|
||||
user_id: string
|
||||
user_name: string
|
||||
editing_field: string | null
|
||||
}
|
||||
|
||||
function setupFieldLocking(formId: string) {
|
||||
const channel = supabase.channel(`form:${formId}`, {
|
||||
config: { presence: { key: currentUserId } }
|
||||
})
|
||||
|
||||
channel
|
||||
.on('presence', { event: 'sync' }, () => {
|
||||
const state = channel.presenceState<EditingState>()
|
||||
updateFieldLocks(Object.values(state).flat())
|
||||
})
|
||||
.subscribe()
|
||||
|
||||
return {
|
||||
startEditing: async (fieldName: string) => {
|
||||
await channel.track({
|
||||
user_id: currentUserId,
|
||||
user_name: currentUserName,
|
||||
editing_field: fieldName
|
||||
})
|
||||
},
|
||||
stopEditing: async () => {
|
||||
await channel.track({
|
||||
user_id: currentUserId,
|
||||
user_name: currentUserName,
|
||||
editing_field: null
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Broadcast Messages
|
||||
|
||||
Send arbitrary messages to channel subscribers:
|
||||
|
||||
```typescript
|
||||
const channel = supabase.channel('room:chat')
|
||||
|
||||
// Subscribe to broadcasts
|
||||
channel
|
||||
.on('broadcast', { event: 'message' }, ({ payload }) => {
|
||||
console.log('Received:', payload)
|
||||
})
|
||||
.subscribe()
|
||||
|
||||
// Send broadcast
|
||||
await channel.send({
|
||||
type: 'broadcast',
|
||||
event: 'message',
|
||||
payload: { text: 'Hello everyone!', sender: userId }
|
||||
})
|
||||
```
|
||||
|
||||
## Subscription Management
|
||||
|
||||
### Unsubscribe
|
||||
|
||||
```typescript
|
||||
// Unsubscribe from specific channel
|
||||
await supabase.removeChannel(channel)
|
||||
|
||||
// Unsubscribe from all channels
|
||||
await supabase.removeAllChannels()
|
||||
```
|
||||
|
||||
### Subscription Status
|
||||
|
||||
```typescript
|
||||
channel.subscribe((status) => {
|
||||
switch (status) {
|
||||
case 'SUBSCRIBED':
|
||||
console.log('Connected to channel')
|
||||
break
|
||||
case 'CLOSED':
|
||||
console.log('Channel closed')
|
||||
break
|
||||
case 'CHANNEL_ERROR':
|
||||
console.log('Channel error')
|
||||
break
|
||||
case 'TIMED_OUT':
|
||||
console.log('Connection timed out')
|
||||
break
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## React Integration
|
||||
|
||||
### Custom Hook for Presence
|
||||
|
||||
```typescript
|
||||
import { useEffect, useState } from 'react'
|
||||
import { supabase } from './supabase/client'
|
||||
|
||||
export function usePresence<T>(channelName: string, userId: string, initialState: T) {
|
||||
const [presences, setPresences] = useState<Record<string, T[]>>({})
|
||||
|
||||
useEffect(() => {
|
||||
const channel = supabase.channel(channelName, {
|
||||
config: { presence: { key: userId } }
|
||||
})
|
||||
|
||||
channel
|
||||
.on('presence', { event: 'sync' }, () => {
|
||||
setPresences(channel.presenceState<T>())
|
||||
})
|
||||
.subscribe(async (status) => {
|
||||
if (status === 'SUBSCRIBED') {
|
||||
await channel.track(initialState)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel)
|
||||
}
|
||||
}, [channelName, userId])
|
||||
|
||||
const updatePresence = async (state: Partial<T>) => {
|
||||
const channel = supabase.getChannels().find(c => c.topic === channelName)
|
||||
if (channel) {
|
||||
await channel.track({ ...initialState, ...state } as T)
|
||||
}
|
||||
}
|
||||
|
||||
return { presences, updatePresence }
|
||||
}
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
function CollaborativeEditor({ documentId, userId }) {
|
||||
const { presences, updatePresence } = usePresence(
|
||||
`doc:${documentId}`,
|
||||
userId,
|
||||
{ user_id: userId, typing: false, cursor: null }
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{Object.values(presences).flat().map(p => (
|
||||
<Cursor key={p.user_id} position={p.cursor} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Context7 Query Examples
|
||||
|
||||
For latest real-time documentation:
|
||||
|
||||
Topic: "realtime postgres_changes subscription"
|
||||
Topic: "presence tracking channel"
|
||||
Topic: "broadcast messages supabase"
|
||||
|
||||
---
|
||||
|
||||
Related Modules:
|
||||
- typescript-patterns.md - Client architecture
|
||||
- auth-integration.md - Authenticated subscriptions
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
---
|
||||
name: row-level-security
|
||||
description: RLS policies for multi-tenant data isolation and access control
|
||||
parent-skill: moai-platform-supabase
|
||||
version: 1.0.0
|
||||
updated: 2026-01-06
|
||||
---
|
||||
|
||||
# Row-Level Security (RLS) Module
|
||||
|
||||
## Overview
|
||||
|
||||
Row-Level Security provides automatic data isolation at the database level, ensuring users can only access data they are authorized to see.
|
||||
|
||||
## Basic Setup
|
||||
|
||||
Enable RLS on a table:
|
||||
|
||||
```sql
|
||||
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
|
||||
```
|
||||
|
||||
## Policy Types
|
||||
|
||||
RLS policies can be created for specific operations:
|
||||
|
||||
- SELECT: Controls read access
|
||||
- INSERT: Controls creation
|
||||
- UPDATE: Controls modification
|
||||
- DELETE: Controls removal
|
||||
- ALL: Applies to all operations
|
||||
|
||||
## Basic Tenant Isolation
|
||||
|
||||
### JWT-Based Tenant Isolation
|
||||
|
||||
Extract tenant ID from JWT claims:
|
||||
|
||||
```sql
|
||||
CREATE POLICY "tenant_isolation" ON projects FOR ALL
|
||||
USING (tenant_id = (auth.jwt() ->> 'tenant_id')::UUID);
|
||||
```
|
||||
|
||||
### Owner-Based Access
|
||||
|
||||
Restrict access to resource owners:
|
||||
|
||||
```sql
|
||||
CREATE POLICY "owner_access" ON projects FOR ALL
|
||||
USING (owner_id = auth.uid());
|
||||
```
|
||||
|
||||
## Hierarchical Access Patterns
|
||||
|
||||
### Organization Membership
|
||||
|
||||
Allow access based on organization membership:
|
||||
|
||||
```sql
|
||||
CREATE POLICY "org_member_select" ON organizations FOR SELECT
|
||||
USING (id IN (SELECT org_id FROM org_members WHERE user_id = auth.uid()));
|
||||
```
|
||||
|
||||
### Role-Based Modification
|
||||
|
||||
Restrict modifications to specific roles:
|
||||
|
||||
```sql
|
||||
CREATE POLICY "org_admin_modify" ON organizations FOR UPDATE
|
||||
USING (id IN (
|
||||
SELECT org_id FROM org_members
|
||||
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
|
||||
));
|
||||
```
|
||||
|
||||
### Cascading Project Access
|
||||
|
||||
Grant project access through organization membership:
|
||||
|
||||
```sql
|
||||
CREATE POLICY "project_access" ON projects FOR ALL
|
||||
USING (org_id IN (SELECT org_id FROM org_members WHERE user_id = auth.uid()));
|
||||
```
|
||||
|
||||
## Service Role Bypass
|
||||
|
||||
Allow service role to bypass RLS for server-side operations:
|
||||
|
||||
```sql
|
||||
CREATE POLICY "service_bypass" ON organizations FOR ALL TO service_role USING (true);
|
||||
```
|
||||
|
||||
## Multi-Tenant SaaS Schema
|
||||
|
||||
### Complete Schema Setup
|
||||
|
||||
```sql
|
||||
-- Organizations (tenants)
|
||||
CREATE TABLE organizations (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
name TEXT NOT NULL,
|
||||
slug TEXT UNIQUE NOT NULL,
|
||||
plan TEXT DEFAULT 'free' CHECK (plan IN ('free', 'pro', 'enterprise')),
|
||||
settings JSONB DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Organization members with roles
|
||||
CREATE TABLE organization_members (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL,
|
||||
role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
|
||||
joined_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(organization_id, user_id)
|
||||
);
|
||||
|
||||
-- Projects within organizations
|
||||
CREATE TABLE projects (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
owner_id UUID NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### Enable RLS on All Tables
|
||||
|
||||
```sql
|
||||
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
|
||||
```
|
||||
|
||||
### Comprehensive RLS Policies
|
||||
|
||||
```sql
|
||||
-- Organization read access
|
||||
CREATE POLICY "org_member_select" ON organizations FOR SELECT
|
||||
USING (id IN (SELECT organization_id FROM organization_members WHERE user_id = auth.uid()));
|
||||
|
||||
-- Organization admin update
|
||||
CREATE POLICY "org_admin_update" ON organizations FOR UPDATE
|
||||
USING (id IN (SELECT organization_id FROM organization_members
|
||||
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')));
|
||||
|
||||
-- Project member access
|
||||
CREATE POLICY "project_member_access" ON projects FOR ALL
|
||||
USING (organization_id IN (SELECT organization_id FROM organization_members WHERE user_id = auth.uid()));
|
||||
|
||||
-- Member management (admin only)
|
||||
CREATE POLICY "member_admin_manage" ON organization_members FOR ALL
|
||||
USING (organization_id IN (SELECT organization_id FROM organization_members
|
||||
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')));
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
|
||||
### Check Organization Membership
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION is_org_member(org_id UUID)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM organization_members
|
||||
WHERE organization_id = org_id AND user_id = auth.uid()
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
```
|
||||
|
||||
### Check Organization Role
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION has_org_role(org_id UUID, required_roles TEXT[])
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM organization_members
|
||||
WHERE organization_id = org_id
|
||||
AND user_id = auth.uid()
|
||||
AND role = ANY(required_roles)
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
```
|
||||
|
||||
### Usage in Policies
|
||||
|
||||
```sql
|
||||
CREATE POLICY "project_admin_delete" ON projects FOR DELETE
|
||||
USING (has_org_role(organization_id, ARRAY['owner', 'admin']));
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Index for RLS Queries
|
||||
|
||||
Create indexes on foreign keys used in RLS policies:
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_org_members_user ON organization_members(user_id);
|
||||
CREATE INDEX idx_org_members_org ON organization_members(organization_id);
|
||||
CREATE INDEX idx_projects_org ON projects(organization_id);
|
||||
```
|
||||
|
||||
### Materialized View for Complex Policies
|
||||
|
||||
For complex permission checks, use materialized views:
|
||||
|
||||
```sql
|
||||
CREATE MATERIALIZED VIEW user_accessible_projects AS
|
||||
SELECT p.id as project_id, om.user_id, om.role
|
||||
FROM projects p
|
||||
JOIN organization_members om ON p.organization_id = om.organization_id;
|
||||
|
||||
CREATE INDEX idx_uap_user ON user_accessible_projects(user_id);
|
||||
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY user_accessible_projects;
|
||||
```
|
||||
|
||||
## Testing RLS Policies
|
||||
|
||||
### Test as Authenticated User
|
||||
|
||||
```sql
|
||||
SET request.jwt.claim.sub = 'user-uuid-here';
|
||||
SET request.jwt.claims = '{"role": "authenticated"}';
|
||||
|
||||
SELECT * FROM projects; -- Returns only accessible projects
|
||||
```
|
||||
|
||||
### Verify Policy Restrictions
|
||||
|
||||
```sql
|
||||
-- Should fail if not a member
|
||||
INSERT INTO projects (organization_id, name, owner_id)
|
||||
VALUES ('non-member-org-id', 'Test', auth.uid());
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Public Read, Owner Write
|
||||
|
||||
```sql
|
||||
CREATE POLICY "public_read" ON posts FOR SELECT USING (true);
|
||||
CREATE POLICY "owner_write" ON posts FOR INSERT WITH CHECK (author_id = auth.uid());
|
||||
CREATE POLICY "owner_update" ON posts FOR UPDATE USING (author_id = auth.uid());
|
||||
CREATE POLICY "owner_delete" ON posts FOR DELETE USING (author_id = auth.uid());
|
||||
```
|
||||
|
||||
### Draft vs Published
|
||||
|
||||
```sql
|
||||
CREATE POLICY "published_read" ON articles FOR SELECT
|
||||
USING (status = 'published' OR author_id = auth.uid());
|
||||
```
|
||||
|
||||
### Time-Based Access
|
||||
|
||||
```sql
|
||||
CREATE POLICY "active_subscription" ON premium_content FOR SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM subscriptions
|
||||
WHERE user_id = auth.uid()
|
||||
AND expires_at > NOW()
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## Context7 Query Examples
|
||||
|
||||
For latest RLS documentation:
|
||||
|
||||
Topic: "row level security policies supabase"
|
||||
Topic: "auth.uid auth.jwt functions"
|
||||
Topic: "rls performance optimization"
|
||||
|
||||
---
|
||||
|
||||
Related Modules:
|
||||
- auth-integration.md - Authentication patterns
|
||||
- typescript-patterns.md - Client-side access patterns
|
||||
319
.claude/skills/moai-platform-supabase/modules/storage-cdn.md
Normal file
319
.claude/skills/moai-platform-supabase/modules/storage-cdn.md
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
---
|
||||
name: storage-cdn
|
||||
description: File storage with image transformations and CDN delivery
|
||||
parent-skill: moai-platform-supabase
|
||||
version: 1.0.0
|
||||
updated: 2026-01-06
|
||||
---
|
||||
|
||||
# Storage and CDN Module
|
||||
|
||||
## Overview
|
||||
|
||||
Supabase Storage provides file storage with automatic image transformations, CDN delivery, and fine-grained access control through storage policies.
|
||||
|
||||
## Basic Upload
|
||||
|
||||
### Upload File
|
||||
|
||||
```typescript
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
|
||||
|
||||
async function uploadFile(file: File, bucket: string, path: string) {
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.upload(path, file, {
|
||||
cacheControl: '3600',
|
||||
upsert: false
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data.path
|
||||
}
|
||||
```
|
||||
|
||||
### Upload with User Context
|
||||
|
||||
```typescript
|
||||
async function uploadUserFile(file: File, userId: string) {
|
||||
const fileName = `${userId}/${Date.now()}-${file.name}`
|
||||
|
||||
const { data, error } = await supabase.storage
|
||||
.from('user-files')
|
||||
.upload(fileName, file, {
|
||||
cacheControl: '3600',
|
||||
upsert: false
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
## Image Transformations
|
||||
|
||||
### Get Transformed URL
|
||||
|
||||
```typescript
|
||||
async function uploadImage(file: File, userId: string) {
|
||||
const fileName = `${userId}/${Date.now()}-${file.name}`
|
||||
|
||||
const { data, error } = await supabase.storage
|
||||
.from('images')
|
||||
.upload(fileName, file, { cacheControl: '3600', upsert: false })
|
||||
|
||||
if (error) throw error
|
||||
|
||||
// Get original URL
|
||||
const { data: { publicUrl } } = supabase.storage
|
||||
.from('images')
|
||||
.getPublicUrl(fileName)
|
||||
|
||||
// Get resized URL
|
||||
const { data: { publicUrl: resizedUrl } } = supabase.storage
|
||||
.from('images')
|
||||
.getPublicUrl(fileName, {
|
||||
transform: { width: 800, height: 600, resize: 'contain' }
|
||||
})
|
||||
|
||||
// Get thumbnail URL
|
||||
const { data: { publicUrl: thumbnailUrl } } = supabase.storage
|
||||
.from('images')
|
||||
.getPublicUrl(fileName, {
|
||||
transform: { width: 200, height: 200, resize: 'cover' }
|
||||
})
|
||||
|
||||
return { originalPath: data.path, publicUrl, resizedUrl, thumbnailUrl }
|
||||
}
|
||||
```
|
||||
|
||||
### Transform Options
|
||||
|
||||
Available transformation parameters:
|
||||
|
||||
- width: Target width in pixels
|
||||
- height: Target height in pixels
|
||||
- resize: 'cover' | 'contain' | 'fill'
|
||||
- format: 'origin' | 'avif' | 'webp'
|
||||
- quality: 1-100
|
||||
|
||||
### Example Transforms
|
||||
|
||||
```typescript
|
||||
// Square thumbnail with crop
|
||||
const thumbnail = supabase.storage
|
||||
.from('images')
|
||||
.getPublicUrl(path, {
|
||||
transform: { width: 150, height: 150, resize: 'cover' }
|
||||
})
|
||||
|
||||
// WebP format for smaller size
|
||||
const webp = supabase.storage
|
||||
.from('images')
|
||||
.getPublicUrl(path, {
|
||||
transform: { width: 800, format: 'webp', quality: 80 }
|
||||
})
|
||||
|
||||
// Responsive image
|
||||
const responsive = supabase.storage
|
||||
.from('images')
|
||||
.getPublicUrl(path, {
|
||||
transform: { width: 400, resize: 'contain' }
|
||||
})
|
||||
```
|
||||
|
||||
## Bucket Management
|
||||
|
||||
### Create Bucket
|
||||
|
||||
```sql
|
||||
-- Via SQL
|
||||
INSERT INTO storage.buckets (id, name, public)
|
||||
VALUES ('images', 'images', true);
|
||||
```
|
||||
|
||||
### Bucket Policies
|
||||
|
||||
```sql
|
||||
-- Allow authenticated users to upload to their folder
|
||||
CREATE POLICY "User upload" ON storage.objects
|
||||
FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (bucket_id = 'user-files' AND (storage.foldername(name))[1] = auth.uid()::text);
|
||||
|
||||
-- Allow public read on images bucket
|
||||
CREATE POLICY "Public read" ON storage.objects
|
||||
FOR SELECT
|
||||
TO public
|
||||
USING (bucket_id = 'images');
|
||||
|
||||
-- Allow users to delete their own files
|
||||
CREATE POLICY "User delete" ON storage.objects
|
||||
FOR DELETE
|
||||
TO authenticated
|
||||
USING (bucket_id = 'user-files' AND (storage.foldername(name))[1] = auth.uid()::text);
|
||||
```
|
||||
|
||||
## Download Files
|
||||
|
||||
### Get Signed URL
|
||||
|
||||
For private buckets:
|
||||
|
||||
```typescript
|
||||
async function getSignedUrl(bucket: string, path: string, expiresIn: number = 3600) {
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.createSignedUrl(path, expiresIn)
|
||||
|
||||
if (error) throw error
|
||||
return data.signedUrl
|
||||
}
|
||||
```
|
||||
|
||||
### Download File
|
||||
|
||||
```typescript
|
||||
async function downloadFile(bucket: string, path: string) {
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.download(path)
|
||||
|
||||
if (error) throw error
|
||||
return data // Blob
|
||||
}
|
||||
```
|
||||
|
||||
## File Management
|
||||
|
||||
### List Files
|
||||
|
||||
```typescript
|
||||
async function listFiles(bucket: string, folder: string) {
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.list(folder, {
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
sortBy: { column: 'created_at', order: 'desc' }
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
### Delete File
|
||||
|
||||
```typescript
|
||||
async function deleteFile(bucket: string, paths: string[]) {
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.remove(paths)
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
### Move/Rename File
|
||||
|
||||
```typescript
|
||||
async function moveFile(bucket: string, fromPath: string, toPath: string) {
|
||||
const { data, error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.move(fromPath, toPath)
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
## React Integration
|
||||
|
||||
### Upload Component
|
||||
|
||||
```typescript
|
||||
function FileUploader({ bucket, onUpload }: Props) {
|
||||
const [uploading, setUploading] = useState(false)
|
||||
|
||||
async function handleUpload(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
setUploading(true)
|
||||
try {
|
||||
const path = `${Date.now()}-${file.name}`
|
||||
const { error } = await supabase.storage
|
||||
.from(bucket)
|
||||
.upload(path, file)
|
||||
|
||||
if (error) throw error
|
||||
onUpload(path)
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
type="file"
|
||||
onChange={handleUpload}
|
||||
disabled={uploading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Image with Fallback
|
||||
|
||||
```typescript
|
||||
function StorageImage({ path, bucket, width, height, fallback }: Props) {
|
||||
const { data: { publicUrl } } = supabase.storage
|
||||
.from(bucket)
|
||||
.getPublicUrl(path, {
|
||||
transform: { width, height, resize: 'cover' }
|
||||
})
|
||||
|
||||
return (
|
||||
<img
|
||||
src={publicUrl}
|
||||
alt=""
|
||||
onError={(e) => { e.currentTarget.src = fallback }}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
File Organization:
|
||||
- Use user ID as folder prefix for user content
|
||||
- Include timestamp in filenames to prevent collisions
|
||||
- Use consistent naming conventions
|
||||
|
||||
Performance:
|
||||
- Set appropriate cache-control headers
|
||||
- Use image transformations instead of storing multiple sizes
|
||||
- Leverage CDN for global delivery
|
||||
|
||||
Security:
|
||||
- Always use RLS-style policies for storage
|
||||
- Use signed URLs for private content
|
||||
- Validate file types before upload
|
||||
|
||||
## Context7 Query Examples
|
||||
|
||||
For latest Storage documentation:
|
||||
|
||||
Topic: "supabase storage upload download"
|
||||
Topic: "storage image transformations"
|
||||
Topic: "storage bucket policies"
|
||||
|
||||
---
|
||||
|
||||
Related Modules:
|
||||
- row-level-security.md - Storage access policies
|
||||
- typescript-patterns.md - Client patterns
|
||||
|
|
@ -0,0 +1,453 @@
|
|||
---
|
||||
name: typescript-patterns
|
||||
description: TypeScript client patterns and service layer architecture for Supabase
|
||||
parent-skill: moai-platform-supabase
|
||||
version: 1.0.0
|
||||
updated: 2026-01-06
|
||||
---
|
||||
|
||||
# TypeScript Patterns Module
|
||||
|
||||
## Overview
|
||||
|
||||
Type-safe Supabase client patterns for building maintainable full-stack applications with TypeScript.
|
||||
|
||||
## Type Generation
|
||||
|
||||
### Generate Types from Database
|
||||
|
||||
```bash
|
||||
supabase gen types typescript --project-id your-project-id > database.types.ts
|
||||
```
|
||||
|
||||
### Database Types Structure
|
||||
|
||||
```typescript
|
||||
// database.types.ts
|
||||
export type Database = {
|
||||
public: {
|
||||
Tables: {
|
||||
projects: {
|
||||
Row: {
|
||||
id: string
|
||||
name: string
|
||||
organization_id: string
|
||||
owner_id: string
|
||||
created_at: string
|
||||
}
|
||||
Insert: {
|
||||
id?: string
|
||||
name: string
|
||||
organization_id: string
|
||||
owner_id: string
|
||||
created_at?: string
|
||||
}
|
||||
Update: {
|
||||
id?: string
|
||||
name?: string
|
||||
organization_id?: string
|
||||
owner_id?: string
|
||||
created_at?: string
|
||||
}
|
||||
}
|
||||
// ... other tables
|
||||
}
|
||||
Functions: {
|
||||
search_documents: {
|
||||
Args: {
|
||||
query_embedding: number[]
|
||||
match_threshold: number
|
||||
match_count: number
|
||||
}
|
||||
Returns: { id: string; content: string; similarity: number }[]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Client Configuration
|
||||
|
||||
### Browser Client with Types
|
||||
|
||||
```typescript
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { Database } from './database.types'
|
||||
|
||||
export const supabase = createClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
)
|
||||
```
|
||||
|
||||
### Server Client (Next.js App Router)
|
||||
|
||||
```typescript
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
import { Database } from './database.types'
|
||||
|
||||
export function createServerSupabase() {
|
||||
const cookieStore = cookies()
|
||||
|
||||
return createServerClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return cookieStore.get(name)?.value
|
||||
},
|
||||
set(name, value, options) {
|
||||
cookieStore.set({ name, value, ...options })
|
||||
},
|
||||
remove(name, options) {
|
||||
cookieStore.set({ name, value: '', ...options })
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Service Layer Pattern
|
||||
|
||||
### Base Service
|
||||
|
||||
```typescript
|
||||
import { supabase } from './supabase/client'
|
||||
import { Database } from './database.types'
|
||||
|
||||
type Tables = Database['public']['Tables']
|
||||
|
||||
export abstract class BaseService<T extends keyof Tables> {
|
||||
constructor(protected tableName: T) {}
|
||||
|
||||
async findAll() {
|
||||
const { data, error } = await supabase
|
||||
.from(this.tableName)
|
||||
.select('*')
|
||||
|
||||
if (error) throw error
|
||||
return data as Tables[T]['Row'][]
|
||||
}
|
||||
|
||||
async findById(id: string) {
|
||||
const { data, error } = await supabase
|
||||
.from(this.tableName)
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
return data as Tables[T]['Row']
|
||||
}
|
||||
|
||||
async create(item: Tables[T]['Insert']) {
|
||||
const { data, error } = await supabase
|
||||
.from(this.tableName)
|
||||
.insert(item)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
return data as Tables[T]['Row']
|
||||
}
|
||||
|
||||
async update(id: string, item: Tables[T]['Update']) {
|
||||
const { data, error } = await supabase
|
||||
.from(this.tableName)
|
||||
.update(item)
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
return data as Tables[T]['Row']
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
const { error } = await supabase
|
||||
.from(this.tableName)
|
||||
.delete()
|
||||
.eq('id', id)
|
||||
|
||||
if (error) throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Document Service with Embeddings
|
||||
|
||||
```typescript
|
||||
import { supabase } from './supabase/client'
|
||||
|
||||
export class DocumentService {
|
||||
async create(projectId: string, title: string, content: string) {
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('documents')
|
||||
.insert({
|
||||
project_id: projectId,
|
||||
title,
|
||||
content,
|
||||
created_by: user!.id
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
|
||||
// Generate embedding asynchronously
|
||||
await supabase.functions.invoke('generate-embedding', {
|
||||
body: { documentId: data.id, content }
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async semanticSearch(projectId: string, query: string) {
|
||||
// Get embedding for query
|
||||
const { data: embeddingData } = await supabase.functions.invoke(
|
||||
'get-embedding',
|
||||
{ body: { text: query } }
|
||||
)
|
||||
|
||||
// Search using RPC
|
||||
const { data, error } = await supabase.rpc('search_documents', {
|
||||
p_project_id: projectId,
|
||||
p_query_embedding: embeddingData.embedding,
|
||||
p_match_threshold: 0.7,
|
||||
p_match_count: 10
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
async findByProject(projectId: string) {
|
||||
const { data, error } = await supabase
|
||||
.from('documents')
|
||||
.select('*, created_by_user:profiles!created_by(*)')
|
||||
.eq('project_id', projectId)
|
||||
.order('created_at', { ascending: false })
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
subscribeToChanges(projectId: string, callback: (payload: any) => void) {
|
||||
return supabase.channel(`documents:${projectId}`)
|
||||
.on('postgres_changes',
|
||||
{
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'documents',
|
||||
filter: `project_id=eq.${projectId}`
|
||||
},
|
||||
callback
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
export const documentService = new DocumentService()
|
||||
```
|
||||
|
||||
## React Query Integration
|
||||
|
||||
### Query Keys
|
||||
|
||||
```typescript
|
||||
export const queryKeys = {
|
||||
projects: {
|
||||
all: ['projects'] as const,
|
||||
list: (filters?: ProjectFilters) => [...queryKeys.projects.all, 'list', filters] as const,
|
||||
detail: (id: string) => [...queryKeys.projects.all, 'detail', id] as const
|
||||
},
|
||||
documents: {
|
||||
all: ['documents'] as const,
|
||||
list: (projectId: string) => [...queryKeys.documents.all, 'list', projectId] as const,
|
||||
search: (projectId: string, query: string) =>
|
||||
[...queryKeys.documents.all, 'search', projectId, query] as const
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Hooks
|
||||
|
||||
```typescript
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { documentService } from '@/services/document-service'
|
||||
|
||||
export function useDocuments(projectId: string) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.documents.list(projectId),
|
||||
queryFn: () => documentService.findByProject(projectId)
|
||||
})
|
||||
}
|
||||
|
||||
export function useSemanticSearch(projectId: string, query: string) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.documents.search(projectId, query),
|
||||
queryFn: () => documentService.semanticSearch(projectId, query),
|
||||
enabled: query.length > 2
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateDocument(projectId: string) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ title, content }: { title: string; content: string }) =>
|
||||
documentService.create(projectId, title, content),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.documents.list(projectId)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Real-time with React
|
||||
|
||||
### Subscription Hook
|
||||
|
||||
```typescript
|
||||
import { useEffect } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
export function useRealtimeDocuments(projectId: string) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
useEffect(() => {
|
||||
const channel = supabase
|
||||
.channel(`documents:${projectId}`)
|
||||
.on('postgres_changes',
|
||||
{
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'documents',
|
||||
filter: `project_id=eq.${projectId}`
|
||||
},
|
||||
() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.documents.list(projectId)
|
||||
})
|
||||
}
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel)
|
||||
}
|
||||
}, [projectId, queryClient])
|
||||
}
|
||||
```
|
||||
|
||||
### Optimistic Updates
|
||||
|
||||
```typescript
|
||||
export function useUpdateDocument() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, updates }: { id: string; updates: Partial<Document> }) => {
|
||||
const { data, error } = await supabase
|
||||
.from('documents')
|
||||
.update(updates)
|
||||
.eq('id', id)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
},
|
||||
onMutate: async ({ id, updates }) => {
|
||||
await queryClient.cancelQueries({ queryKey: ['documents'] })
|
||||
|
||||
const previousDocuments = queryClient.getQueryData(['documents'])
|
||||
|
||||
queryClient.setQueryData(['documents'], (old: Document[]) =>
|
||||
old.map(doc => doc.id === id ? { ...doc, ...updates } : doc)
|
||||
)
|
||||
|
||||
return { previousDocuments }
|
||||
},
|
||||
onError: (err, variables, context) => {
|
||||
if (context?.previousDocuments) {
|
||||
queryClient.setQueryData(['documents'], context.previousDocuments)
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['documents'] })
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Custom Error Types
|
||||
|
||||
```typescript
|
||||
export class SupabaseError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public code: string,
|
||||
public details?: unknown
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'SupabaseError'
|
||||
}
|
||||
}
|
||||
|
||||
export function handleSupabaseError(error: PostgrestError): never {
|
||||
switch (error.code) {
|
||||
case '23505':
|
||||
throw new SupabaseError('Resource already exists', 'DUPLICATE', error)
|
||||
case '23503':
|
||||
throw new SupabaseError('Referenced resource not found', 'NOT_FOUND', error)
|
||||
case 'PGRST116':
|
||||
throw new SupabaseError('Resource not found', 'NOT_FOUND', error)
|
||||
default:
|
||||
throw new SupabaseError(error.message, error.code, error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service with Error Handling
|
||||
|
||||
```typescript
|
||||
async findById(id: string) {
|
||||
const { data, error } = await supabase
|
||||
.from(this.tableName)
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
|
||||
if (error) {
|
||||
handleSupabaseError(error)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
## Context7 Query Examples
|
||||
|
||||
For latest client documentation:
|
||||
|
||||
Topic: "supabase-js typescript client"
|
||||
Topic: "supabase ssr next.js app router"
|
||||
Topic: "supabase realtime subscription"
|
||||
|
||||
---
|
||||
|
||||
Related Modules:
|
||||
- auth-integration.md - Auth patterns
|
||||
- realtime-presence.md - Real-time subscriptions
|
||||
- postgresql-pgvector.md - Database operations
|
||||
284
.claude/skills/moai-platform-supabase/reference.md
Normal file
284
.claude/skills/moai-platform-supabase/reference.md
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
---
|
||||
name: supabase-reference
|
||||
description: API reference summary for Supabase platform
|
||||
parent-skill: moai-platform-supabase
|
||||
version: 1.0.0
|
||||
updated: 2026-01-06
|
||||
---
|
||||
|
||||
# Supabase API Reference
|
||||
|
||||
## Client Methods
|
||||
|
||||
### Database Operations
|
||||
|
||||
```typescript
|
||||
// Select
|
||||
const { data, error } = await supabase.from('table').select('*')
|
||||
const { data } = await supabase.from('table').select('id, name, relation(*)').eq('id', 1)
|
||||
|
||||
// Insert
|
||||
const { data, error } = await supabase.from('table').insert({ column: 'value' }).select()
|
||||
const { data } = await supabase.from('table').insert([...items]).select()
|
||||
|
||||
// Update
|
||||
const { data, error } = await supabase.from('table').update({ column: 'value' }).eq('id', 1).select()
|
||||
|
||||
// Upsert
|
||||
const { data, error } = await supabase.from('table').upsert({ id: 1, column: 'value' }).select()
|
||||
|
||||
// Delete
|
||||
const { error } = await supabase.from('table').delete().eq('id', 1)
|
||||
```
|
||||
|
||||
### Query Filters
|
||||
|
||||
```typescript
|
||||
.eq('column', 'value') // Equal
|
||||
.neq('column', 'value') // Not equal
|
||||
.gt('column', 0) // Greater than
|
||||
.gte('column', 0) // Greater than or equal
|
||||
.lt('column', 100) // Less than
|
||||
.lte('column', 100) // Less than or equal
|
||||
.like('column', '%pattern%') // LIKE
|
||||
.ilike('column', '%pattern%') // ILIKE (case insensitive)
|
||||
.is('column', null) // IS NULL
|
||||
.in('column', ['a', 'b']) // IN
|
||||
.contains('array_col', ['a']) // Array contains
|
||||
.containedBy('col', ['a','b']) // Array contained by
|
||||
.range('col', '[1,10)') // Range
|
||||
.textSearch('col', 'query') // Full-text search
|
||||
.filter('col', 'op', 'val') // Generic filter
|
||||
```
|
||||
|
||||
### Query Modifiers
|
||||
|
||||
```typescript
|
||||
.order('column', { ascending: false })
|
||||
.limit(10)
|
||||
.range(0, 9) // Pagination
|
||||
.single() // Expect exactly one row
|
||||
.maybeSingle() // Expect zero or one row
|
||||
.count('exact', { head: true }) // Count only
|
||||
```
|
||||
|
||||
### RPC (Remote Procedure Call)
|
||||
|
||||
```typescript
|
||||
const { data, error } = await supabase.rpc('function_name', {
|
||||
arg1: 'value1',
|
||||
arg2: 'value2'
|
||||
})
|
||||
```
|
||||
|
||||
## Auth Methods
|
||||
|
||||
```typescript
|
||||
// Sign up
|
||||
await supabase.auth.signUp({ email, password })
|
||||
|
||||
// Sign in
|
||||
await supabase.auth.signInWithPassword({ email, password })
|
||||
await supabase.auth.signInWithOAuth({ provider: 'google' })
|
||||
await supabase.auth.signInWithOtp({ email })
|
||||
|
||||
// Session
|
||||
await supabase.auth.getUser()
|
||||
await supabase.auth.getSession()
|
||||
await supabase.auth.refreshSession()
|
||||
|
||||
// Sign out
|
||||
await supabase.auth.signOut()
|
||||
|
||||
// Password
|
||||
await supabase.auth.resetPasswordForEmail(email)
|
||||
await supabase.auth.updateUser({ password: newPassword })
|
||||
|
||||
// Listener
|
||||
supabase.auth.onAuthStateChange((event, session) => {})
|
||||
```
|
||||
|
||||
## Real-time Methods
|
||||
|
||||
```typescript
|
||||
// Subscribe to changes
|
||||
const channel = supabase.channel('channel-name')
|
||||
.on('postgres_changes',
|
||||
{ event: '*', schema: 'public', table: 'table_name' },
|
||||
(payload) => {}
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
// Presence
|
||||
channel.on('presence', { event: 'sync' }, () => {
|
||||
const state = channel.presenceState()
|
||||
})
|
||||
await channel.track({ user_id: 'id', online_at: new Date() })
|
||||
|
||||
// Broadcast
|
||||
await channel.send({ type: 'broadcast', event: 'name', payload: {} })
|
||||
channel.on('broadcast', { event: 'name' }, ({ payload }) => {})
|
||||
|
||||
// Unsubscribe
|
||||
await supabase.removeChannel(channel)
|
||||
await supabase.removeAllChannels()
|
||||
```
|
||||
|
||||
## Storage Methods
|
||||
|
||||
```typescript
|
||||
// Upload
|
||||
await supabase.storage.from('bucket').upload('path/file.ext', file, { cacheControl: '3600' })
|
||||
|
||||
// Download
|
||||
await supabase.storage.from('bucket').download('path/file.ext')
|
||||
|
||||
// Get URL
|
||||
supabase.storage.from('bucket').getPublicUrl('path/file.ext', {
|
||||
transform: { width: 800, height: 600, resize: 'cover' }
|
||||
})
|
||||
|
||||
// Signed URL
|
||||
await supabase.storage.from('bucket').createSignedUrl('path/file.ext', 3600)
|
||||
|
||||
// List
|
||||
await supabase.storage.from('bucket').list('folder', { limit: 100 })
|
||||
|
||||
// Delete
|
||||
await supabase.storage.from('bucket').remove(['path/file.ext'])
|
||||
|
||||
// Move
|
||||
await supabase.storage.from('bucket').move('old/path', 'new/path')
|
||||
```
|
||||
|
||||
## Edge Functions
|
||||
|
||||
```typescript
|
||||
// Invoke
|
||||
const { data, error } = await supabase.functions.invoke('function-name', {
|
||||
body: { key: 'value' },
|
||||
headers: { 'Custom-Header': 'value' }
|
||||
})
|
||||
```
|
||||
|
||||
## SQL Quick Reference
|
||||
|
||||
### pgvector
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION vector;
|
||||
|
||||
-- Create table with vector column
|
||||
CREATE TABLE items (
|
||||
id UUID PRIMARY KEY,
|
||||
embedding vector(1536)
|
||||
);
|
||||
|
||||
-- HNSW index (recommended)
|
||||
CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops);
|
||||
|
||||
-- IVFFlat index (large datasets)
|
||||
CREATE INDEX ON items USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
|
||||
|
||||
-- Distance operators
|
||||
<-> -- Euclidean distance
|
||||
<=> -- Cosine distance
|
||||
<#> -- Negative inner product
|
||||
```
|
||||
|
||||
### Row-Level Security
|
||||
|
||||
```sql
|
||||
-- Enable RLS
|
||||
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Create policy
|
||||
CREATE POLICY "policy_name" ON table_name
|
||||
FOR SELECT | INSERT | UPDATE | DELETE | ALL
|
||||
TO role_name
|
||||
USING (expression)
|
||||
WITH CHECK (expression);
|
||||
|
||||
-- Auth functions
|
||||
auth.uid() -- Current user ID
|
||||
auth.jwt() ->> 'claim' -- JWT claim value
|
||||
auth.role() -- Current role
|
||||
```
|
||||
|
||||
### Useful Functions
|
||||
|
||||
```sql
|
||||
gen_random_uuid() -- Generate UUID
|
||||
uuid_generate_v4() -- Generate UUID (requires uuid-ossp)
|
||||
NOW() -- Current timestamp
|
||||
CURRENT_TIMESTAMP -- Current timestamp
|
||||
to_tsvector('english', text) -- Full-text search vector
|
||||
plainto_tsquery('query') -- Full-text search query
|
||||
ts_rank(vector, query) -- Full-text search rank
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Public (safe for client)
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
|
||||
# Private (server-only)
|
||||
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
SUPABASE_JWT_SECRET=your-jwt-secret
|
||||
```
|
||||
|
||||
## CLI Commands
|
||||
|
||||
```bash
|
||||
# Project
|
||||
supabase init
|
||||
supabase start
|
||||
supabase stop
|
||||
supabase status
|
||||
|
||||
# Database
|
||||
supabase db diff
|
||||
supabase db push
|
||||
supabase db reset
|
||||
supabase migration new migration_name
|
||||
supabase migration list
|
||||
|
||||
# Types
|
||||
supabase gen types typescript --project-id xxx > database.types.ts
|
||||
|
||||
# Functions
|
||||
supabase functions new function-name
|
||||
supabase functions serve function-name
|
||||
supabase functions deploy function-name
|
||||
supabase functions list
|
||||
|
||||
# Secrets
|
||||
supabase secrets set KEY=value
|
||||
supabase secrets list
|
||||
```
|
||||
|
||||
## Context7 Documentation Access
|
||||
|
||||
For detailed API documentation, use Context7 MCP tools:
|
||||
|
||||
```
|
||||
Step 1: Resolve library ID
|
||||
mcp__context7__resolve-library-id with query "supabase"
|
||||
|
||||
Step 2: Fetch documentation
|
||||
mcp__context7__get-library-docs with:
|
||||
- context7CompatibleLibraryID: resolved ID
|
||||
- topic: "specific topic"
|
||||
- tokens: 5000-10000
|
||||
```
|
||||
|
||||
Common topics:
|
||||
- "javascript client select insert update"
|
||||
- "auth signIn signUp oauth"
|
||||
- "realtime postgres_changes presence"
|
||||
- "storage upload download transform"
|
||||
- "edge-functions deploy invoke"
|
||||
- "row-level-security policies"
|
||||
- "pgvector similarity search"
|
||||
Loading…
Reference in New Issue
Block a user