원클릭으로
create-widget
Guide to creating new widgets following iOS Design System patterns and conventions
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Guide to creating new widgets following iOS Design System patterns and conventions
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Specialized widgets and themes for Music, Stocks, Weather, App Store, and Books apps
Button widgets in iOS Design System - CupertinoButtonWidget and ButtonWidget with all variants
Icons, tags, titles, descriptions, dividers, and markdown rendering for iOS Design System
Using flutter_hooks for cleaner state management in iOS Design System widgets
Text fields, switches, pickers, date pickers, and form validation for iOS Design System
ScaffoldWidget, GroupedTableWidget, RowWidget, NavigationBar, and ToolBar for iOS Design System
| name | create-widget |
| description | Guide to creating new widgets following iOS Design System patterns and conventions |
Step-by-step guide to creating widgets that follow the design system's architecture and conventions.
Pattern: {widget_name}_widget.dart
// ✅ Good
scaffold_widget.dart
button_widget.dart
cupertino_text_field_widget.dart
// ❌ Avoid
scaffold.dart
btn_widget.dart
textfield.dart
Pattern: {Name}Widget
class ScaffoldWidget extends StatelessWidget {}
class ButtonWidget extends StatelessWidget {}
class IconWidget extends StatelessWidget {}
// lib/src/widgets/my_custom_widget.dart
import 'package:flutter/cupertino.dart';
import '../../ios_design_system.dart';
/// iOS-style custom widget for [purpose].
///
/// This widget provides [main features] with automatic theme adaptation
/// for both light and dark modes.
///
/// ## Example
///
/// ```dart
/// MyCustomWidget(
/// title: 'Example',
/// subtitle: 'Description',
/// onPressed: () {},
/// )
/// ```
///
/// See also:
///
/// * [RelatedWidget], which provides similar functionality.
class MyCustomWidget extends StatelessWidget {
/// Creates a custom widget.
///
/// The [title] parameter is required and must not be null.
const MyCustomWidget({
required this.title,
this.subtitle,
this.onPressed,
super.key,
});
/// The primary title text.
final String title;
/// Optional subtitle text shown below the title.
final String? subtitle;
/// Called when the widget is tapped.
///
/// If null, the widget will be non-interactive.
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
final theme = IosTheme.of(context);
return CupertinoButtonWidget(
onPressed: onPressed,
color: switch (theme) {
IosLightThemeData() => theme.defaultSystemBackgroundsColors.primaryLight,
IosDarkThemeData() => theme.defaultSystemBackgroundsColors.primaryDarkElevated,
},
borderRadius: BorderRadius.circular(12),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.typography.bodyBold.copyWith(
color: theme.defaultLabelColors.primary,
),
),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle!,
style: theme.typography.caption1Regular.copyWith(
color: theme.defaultLabelColors.secondary,
),
),
],
],
),
);
}
}
import 'package:flutter/cupertino.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import '../../ios_design_system.dart';
class MyHookWidget extends HookWidget {
const MyHookWidget({
required this.initialValue,
super.key,
});
final String initialValue;
@override
Widget build(BuildContext context) {
final theme = IosTheme.of(context);
final controller = useTextEditingController(text: initialValue);
final isValid = useState(false);
useEffect(() {
void validate() {
isValid.value = controller.text.isNotEmpty;
}
controller.addListener(validate);
return () => controller.removeListener(validate);
}, [controller]);
return CupertinoTextFieldWidget(
controller: controller,
placeholder: 'Enter text',
);
}
}
class MyWidget extends StatelessWidget {
const MyWidget._internal({
required this.variant,
required this.content,
super.key,
});
/// Standard variant with default styling
factory MyWidget.standard({
required String content,
}) => MyWidget._internal(
variant: MyWidgetVariant.standard,
content: content,
);
/// Rounded variant with 14pt border radius
factory MyWidget.rounded({
required String content,
}) => MyWidget._internal(
variant: MyWidgetVariant.rounded,
content: content,
);
/// Stocks variant with gradient background
factory MyWidget.stocks({
required String content,
}) => MyWidget._internal(
variant: MyWidgetVariant.stocks,
content: content,
);
final MyWidgetVariant variant;
final String content;
@override
Widget build(BuildContext context) {
final theme = IosTheme.of(context);
return Container(
decoration: switch (variant) {
MyWidgetVariant.standard => BoxDecoration(
color: theme.defaultSystemBackgroundsColors.primaryLight,
),
MyWidgetVariant.rounded => BoxDecoration(
color: theme.defaultSystemBackgroundsColors.primaryLight,
borderRadius: BorderRadius.circular(14),
),
MyWidgetVariant.stocks => theme.stocksDecorations.gradients.background,
},
child: Text(content),
);
}
}
enum MyWidgetVariant {
standard,
rounded,
stocks,
}
ALWAYS access theme via context:
@override
Widget build(BuildContext context) {
final theme = IosTheme.of(context);
// Extract colors
final backgroundColor = switch (theme) {
IosLightThemeData() => theme.defaultSystemBackgroundsColors.primaryLight,
IosDarkThemeData() => theme.defaultSystemBackgroundsColors.primaryDarkElevated,
};
final textColor = theme.defaultLabelColors.primary;
final textStyle = theme.typography.bodyRegular;
return Container(
color: backgroundColor,
child: Text(
'Text',
style: textStyle.copyWith(color: textColor),
),
);
}
For dynamic theme access in widget parameters:
class MyWidget extends StatelessWidget {
const MyWidget({
required this.title,
this.backgroundColorCallback,
this.textColorCallback,
super.key,
});
final String title;
final Color Function(IosThemeData)? backgroundColorCallback;
final Color Function(IosThemeData)? textColorCallback;
@override
Widget build(BuildContext context) {
final theme = IosTheme.of(context);
final backgroundColor = backgroundColorCallback?.call(theme) ??
theme.defaultSystemBackgroundsColors.primaryLight;
final textColor = textColorCallback?.call(theme) ??
theme.defaultLabelColors.primary;
return Container(
color: backgroundColor,
child: Text(
title,
style: theme.typography.bodyRegular.copyWith(color: textColor),
),
);
}
}
// Usage
MyWidget(
title: 'Hello',
backgroundColorCallback: (theme) => theme.defaultColors.systemBlue,
textColorCallback: (theme) => theme.defaultColors.systemWhite,
)
sealed class ButtonColor {
const ButtonColor();
Color? backgroundEnabled({required BuildContext context});
Color? backgroundDisabled({required BuildContext context});
Color labelEnabled({required BuildContext context});
Color labelDisabled({required BuildContext context});
Gradient? backgroundGradientEnabled({required BuildContext context}) => null;
}
class BlueButtonColor extends ButtonColor {
const BlueButtonColor();
@override
Color? backgroundEnabled({required BuildContext context}) {
final theme = IosTheme.of(context);
return theme.defaultColors.systemBlue;
}
@override
Color? backgroundDisabled({required BuildContext context}) {
final theme = IosTheme.of(context);
return theme.defaultFillColors.secondary;
}
@override
Color labelEnabled({required BuildContext context}) {
final theme = IosTheme.of(context);
return theme.defaultColors.systemWhite;
}
@override
Color labelDisabled({required BuildContext context}) {
final theme = IosTheme.of(context);
return theme.defaultLabelColors.tertiary;
}
}
class CustomButtonColor extends ButtonColor {
const CustomButtonColor({
required this.backgroundEnabledColor,
required this.backgroundDisabledColor,
required this.labelEnabledColor,
required this.labelDisabledColor,
this.backgroundGradientEnabledValue,
});
final Color? backgroundEnabledColor;
final Color? backgroundDisabledColor;
final Color labelEnabledColor;
final Color? labelDisabledColor;
final Gradient? backgroundGradientEnabledValue;
@override
Color? backgroundEnabled({required BuildContext context}) =>
backgroundEnabledColor;
@override
Color? backgroundDisabled({required BuildContext context}) =>
backgroundDisabledColor;
@override
Color labelEnabled({required BuildContext context}) =>
labelEnabledColor;
@override
Color labelDisabled({required BuildContext context}) =>
labelDisabledColor ?? labelEnabledColor.withOpacity(0.3);
@override
Gradient? backgroundGradientEnabled({required BuildContext context}) =>
backgroundGradientEnabledValue;
}
Place in /lib/src/widgets/:
/lib/src/widgets/my_custom_widget.dart
Add to /lib/src/widgets/exports.dart:
export 'my_custom_widget.dart';
import 'package:ios_design_system/ios_design_system.dart';
// Now you can use
MyCustomWidget(title: 'Hello')
// Use standard spacing multiples
const EdgeInsets.all(8) // Micro spacing
const EdgeInsets.all(16) // Standard spacing
const EdgeInsets.all(24) // Section spacing
const EdgeInsets.all(32) // Large spacing
const SizedBox(height: 8) // Small gaps
const SizedBox(height: 16) // Standard gaps
const SizedBox(height: 24) // Section gaps
const SizedBox(width: 4) // Between tags
const SizedBox(width: 8) // Between buttons
BorderRadius.circular(7) // Icon backgrounds
BorderRadius.circular(10) // Search fields
BorderRadius.circular(12) // Large buttons, cards
BorderRadius.circular(14) // Grouped tables, small buttons
BorderRadius.circular(50) // Pills, tags, medium buttons
Minimum: 44pt x 44pt
BoxConstraints(
minHeight: 44,
minWidth: 44,
)
// Use theme typography
theme.typography.largeTitleBold // 34pt - Page headers
theme.typography.title1Bold // 28pt - Section titles
theme.typography.title2Bold // 22pt - Subsection titles
theme.typography.title3Bold // 20pt - Card titles
theme.typography.bodyRegular // 17pt - Body text (DEFAULT)
theme.typography.calloutRegular // 16pt - Secondary content
theme.typography.subheadlineRegular // 15pt - Small buttons
theme.typography.footnoteRegular // 13pt - Footnotes
theme.typography.caption1Regular // 12pt - Captions
theme.typography.caption2Regular // 11pt - Smallest text
// test/widgets/my_custom_widget_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart';
import 'package:ios_design_system/ios_design_system.dart';
void main() {
testWidgets('MyCustomWidget displays title', (tester) async {
await tester.pumpWidget(
IosAnimatedTheme(
data: IosLightThemeData(),
child: CupertinoApp(
home: MyCustomWidget(
title: 'Test Title',
),
),
),
);
expect(find.text('Test Title'), findsOneWidget);
});
testWidgets('MyCustomWidget adapts to dark theme', (tester) async {
await tester.pumpWidget(
IosAnimatedTheme(
data: IosDarkThemeData(),
child: CupertinoApp(
home: MyCustomWidget(
title: 'Test',
),
),
),
);
await tester.pumpAndSettle();
// Test dark theme specific behavior
});
testWidgets('MyCustomWidget handles null subtitle', (tester) async {
await tester.pumpWidget(
IosAnimatedTheme(
data: IosLightThemeData(),
child: CupertinoApp(
home: MyCustomWidget(
title: 'Title',
subtitle: null,
),
),
),
);
expect(find.text('Title'), findsOneWidget);
});
}
Before committing your widget:
_widget.dart suffixWidget suffix/lib/src/widgets/exports.dartIosTheme.of(context) for all visual properties// ❌ Bad
Container(
color: Color(0xFF007AFF),
child: child,
)
// ✅ Good
Container(
color: theme.defaultColors.systemBlue,
child: child,
)
// ❌ Bad
Text(
'Hello',
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w400),
)
// ✅ Good
Text(
'Hello',
style: theme.typography.bodyRegular,
)
// ❌ Bad - only works in light mode
Container(
color: theme.defaultSystemBackgroundsColors.primaryLight,
child: child,
)
// ✅ Good - adapts to theme
Container(
color: switch (theme) {
IosLightThemeData() => theme.defaultSystemBackgroundsColors.primaryLight,
IosDarkThemeData() => theme.defaultSystemBackgroundsColors.primaryDarkElevated,
},
child: child,
)
// ❌ Bad
import 'package:flutter/material.dart';
Scaffold(
appBar: AppBar(title: Text('Title')),
body: child,
)
// ✅ Good
import 'package:flutter/cupertino.dart';
ScaffoldWidget(
navigationBar: CupertinoNavigatorBarWidget(
title: 'Title',
),
child: child,
)
// ❌ Bad - too much boilerplate
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
bool _isEnabled = false;
@override
Widget build(BuildContext context) {
return SwitchWidget(
value: _isEnabled,
onChanged: (value) => setState(() => _isEnabled = value),
);
}
}
// ✅ Good - use hooks
class MyWidget extends HookWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
final isEnabled = useState(false);
return SwitchWidget(
value: isEnabled.value,
onChanged: (value) => isEnabled.value = value,
);
}
}
Complete example from start to finish:
/lib/src/widgets/custom_card_widget.dart
import 'package:flutter/cupertino.dart';
import '../../ios_design_system.dart';
/// iOS-style card widget with rounded corners and shadow.
///
/// Displays content in a card format with automatic theme adaptation.
///
/// ## Example
///
/// ```dart
/// CustomCardWidget(
/// title: 'Card Title',
/// description: 'Card description text',
/// icon: CupertinoIcons.star,
/// onPressed: () {},
/// )
/// ```
class CustomCardWidget extends StatelessWidget {
const CustomCardWidget({
required this.title,
this.description,
this.icon,
this.onPressed,
super.key,
});
final String title;
final String? description;
final IconData? icon;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
final theme = IosTheme.of(context);
return CupertinoButtonWidget(
onPressed: onPressed,
padding: EdgeInsets.zero,
color: Colors.transparent,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: switch (theme) {
IosLightThemeData() =>
theme.defaultSystemBackgroundsColors.secondaryLight,
IosDarkThemeData() =>
theme.defaultSystemBackgroundsColors.secondaryDarkElevated,
},
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
if (icon != null) ...[
IconWidget.background(
iconData: icon!,
backgroundColorCallback: (theme) =>
theme.defaultColors.systemBlue,
),
const SizedBox(width: 16),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: theme.typography.bodyBold.copyWith(
color: theme.defaultLabelColors.primary,
),
),
if (description != null) ...[
const SizedBox(height: 4),
Text(
description!,
style: theme.typography.caption1Regular.copyWith(
color: theme.defaultLabelColors.secondary,
),
),
],
],
),
),
if (onPressed != null)
Icon(
CupertinoIcons.chevron_right,
color: theme.defaultLabelColors.tertiary,
size: 20,
),
],
),
),
);
}
}
Add to /lib/src/widgets/exports.dart:
export 'custom_card_widget.dart';
// test/widgets/custom_card_widget_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart';
import 'package:ios_design_system/ios_design_system.dart';
void main() {
testWidgets('CustomCardWidget displays title', (tester) async {
await tester.pumpWidget(
IosAnimatedTheme(
data: IosLightThemeData(),
child: CupertinoApp(
home: CustomCardWidget(
title: 'Test Card',
),
),
),
);
expect(find.text('Test Card'), findsOneWidget);
});
}
// example/lib/main.dart
CustomCardWidget(
title: 'Example Card',
description: 'This is a card widget',
icon: CupertinoIcons.star,
onPressed: () {},
)
IosTheme.of(context)