feat(widgets): add WnProfileSwitcherItem component (#188)
* feat(widgets): add WnProfileSwitcherItem component - Create reusable profile switcher item for switch profile screen - Use WnAvatarSize.medium (56px) per Figma specs - Use WnMiddleEllipsisText for npub display with middle truncation - Add widgetbook showcase with real npub examples - Refactor SwitchProfileScreen to use new component Closes #141 * fix: rename sloth package imports to whitenoise and fix l10n validation - Update wn_profile_switcher_item imports from sloth to whitenoise - Update test and widgetbook imports from sloth to whitenoise - Fix validate-locales-keys.sh to use LC_ALL=C for consistent sorting - Include auto-generated plugin registrant updates * refactor: compute formatted pubkey once in WnProfileSwitcherItem build Extract formatPublicKey(npubFromHex(pubkey) ?? pubkey) into a local final variable to avoid duplicate computation in the build method.
This commit is contained in:
@@ -7,10 +7,9 @@ import 'package:whitenoise/hooks/use_user_metadata.dart';
|
||||
import 'package:whitenoise/providers/auth_provider.dart';
|
||||
import 'package:whitenoise/routes.dart';
|
||||
import 'package:whitenoise/theme.dart';
|
||||
import 'package:whitenoise/utils/formatting.dart';
|
||||
import 'package:whitenoise/utils/metadata.dart';
|
||||
import 'package:whitenoise/widgets/wn_avatar.dart';
|
||||
import 'package:whitenoise/widgets/wn_button.dart';
|
||||
import 'package:whitenoise/widgets/wn_profile_switcher_item.dart';
|
||||
import 'package:whitenoise/widgets/wn_slate.dart';
|
||||
import 'package:whitenoise/widgets/wn_slate_navigation_header.dart';
|
||||
|
||||
@@ -95,9 +94,10 @@ class SwitchProfileScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: accountsList.length,
|
||||
separatorBuilder: (context, index) => Gap(8.h),
|
||||
itemBuilder: (context, index) {
|
||||
final account = accountsList[index];
|
||||
final isCurrentAccount = account.pubkey == currentPubkey;
|
||||
@@ -144,64 +144,16 @@ class _AccountTile extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colors = context.colors;
|
||||
final typography = context.typographyScaled;
|
||||
final metadataSnapshot = useUserMetadata(context, pubkey);
|
||||
final metadata = metadataSnapshot.data;
|
||||
final displayName = presentName(metadata);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: isSwitching ? null : onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h),
|
||||
child: Row(
|
||||
children: [
|
||||
WnAvatar(
|
||||
pictureUrl: metadata?.picture,
|
||||
displayName: displayName,
|
||||
color: AvatarColor.fromPubkey(pubkey),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (displayName != null)
|
||||
Text(
|
||||
displayName,
|
||||
style: typography.semiBold16.copyWith(
|
||||
color: colors.backgroundContentPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
formatPublicKey(npubFromHex(pubkey) ?? pubkey),
|
||||
style: typography.medium12.copyWith(
|
||||
color: colors.backgroundContentSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isCurrent)
|
||||
Icon(
|
||||
key: const Key('current_account_checkmark'),
|
||||
Icons.check_circle,
|
||||
color: colors.backgroundContentPrimary,
|
||||
size: 24.w,
|
||||
),
|
||||
if (isSwitching && !isCurrent)
|
||||
SizedBox(
|
||||
width: 24.w,
|
||||
height: 24.w,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: colors.backgroundContentPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
return WnProfileSwitcherItem(
|
||||
pubkey: pubkey,
|
||||
displayName: displayName,
|
||||
pictureUrl: metadata?.picture,
|
||||
isSelected: isCurrent,
|
||||
onTap: isSwitching ? () {} : onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:gap/gap.dart' show Gap;
|
||||
import 'package:whitenoise/theme.dart';
|
||||
import 'package:whitenoise/utils/formatting.dart';
|
||||
import 'package:whitenoise/widgets/wn_avatar.dart';
|
||||
import 'package:whitenoise/widgets/wn_icon.dart';
|
||||
import 'package:whitenoise/widgets/wn_middle_ellipsis_text.dart';
|
||||
|
||||
class WnProfileSwitcherItem extends StatelessWidget {
|
||||
const WnProfileSwitcherItem({
|
||||
super.key,
|
||||
required this.pubkey,
|
||||
this.displayName,
|
||||
this.pictureUrl,
|
||||
this.isSelected = false,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String pubkey;
|
||||
final String? displayName;
|
||||
final String? pictureUrl;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = context.colors;
|
||||
final typography = context.typographyScaled;
|
||||
final formattedPubkey = formatPublicKey(npubFromHex(pubkey) ?? pubkey);
|
||||
|
||||
final backgroundColor = isSelected ? colors.fillTertiaryActive : colors.fillTertiary;
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.h),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(8.r),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
WnAvatar(
|
||||
pictureUrl: pictureUrl,
|
||||
displayName: displayName,
|
||||
size: WnAvatarSize.medium,
|
||||
color: AvatarColor.fromPubkey(pubkey),
|
||||
),
|
||||
Gap(8.w),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (displayName != null)
|
||||
Text(
|
||||
displayName!,
|
||||
style: typography.medium16.copyWith(
|
||||
color: colors.backgroundContentPrimary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
)
|
||||
else
|
||||
WnMiddleEllipsisText(
|
||||
text: formattedPubkey,
|
||||
style: typography.medium16.copyWith(
|
||||
color: colors.backgroundContentPrimary,
|
||||
),
|
||||
),
|
||||
Gap(4.h),
|
||||
WnMiddleEllipsisText(
|
||||
text: formattedPubkey,
|
||||
style: typography.medium12.copyWith(
|
||||
color: colors.backgroundContentSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSelected) ...[
|
||||
Gap(8.w),
|
||||
WnIcon(
|
||||
WnIcons.checkmark,
|
||||
key: const Key('profile_switcher_item_checkmark'),
|
||||
color: colors.backgroundContentSecondary,
|
||||
size: 18.sp,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
# Use C locale for consistent sorting across different environments
|
||||
export LC_ALL=C
|
||||
|
||||
TEMPLATE="${1:-lib/l10n/app_en.arb}"
|
||||
ERRORS=0
|
||||
MISSING_COUNT=0
|
||||
|
||||
@@ -262,12 +262,11 @@ void main() {
|
||||
await tester.tap(find.text('Display $testPubkeyB'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
expect(find.text('Profiles'), findsOneWidget);
|
||||
|
||||
mockAuthNotifier.switchProfileCompleter!.complete();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
expect(find.text('Failed to switch profile. Please try again.'), findsNothing);
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:whitenoise/src/rust/api/accounts.dart';
|
||||
import 'package:whitenoise/src/rust/frb_generated.dart';
|
||||
import 'package:whitenoise/utils/avatar_color.dart' show AvatarColor;
|
||||
import 'package:whitenoise/widgets/wn_avatar.dart' show WnAvatar;
|
||||
import 'package:whitenoise/widgets/wn_middle_ellipsis_text.dart';
|
||||
|
||||
import '../mocks/mock_secure_storage.dart';
|
||||
import '../mocks/mock_wn_api.dart';
|
||||
@@ -110,7 +111,7 @@ void main() {
|
||||
|
||||
testWidgets('displays checkmark for current account', (tester) async {
|
||||
await pumpSwitchProfileScreen(tester, testPubkeyA);
|
||||
expect(find.byKey(const Key('current_account_checkmark')), findsOneWidget);
|
||||
expect(find.byKey(const Key('profile_switcher_item_checkmark')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('displays Connect Another Profile button', (tester) async {
|
||||
@@ -147,7 +148,7 @@ void main() {
|
||||
expect(find.text('No accounts available'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows loading indicator while switching profile', (tester) async {
|
||||
testWidgets('disables taps while switching profile', (tester) async {
|
||||
final mockAuthNotifier = _MockAuthNotifier(testPubkeyA);
|
||||
mockAuthNotifier.switchProfileCompleter = Completer<void>();
|
||||
|
||||
@@ -160,7 +161,7 @@ void main() {
|
||||
await tester.tap(find.text('Display $testPubkeyB'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||
expect(find.text('Profiles'), findsOneWidget);
|
||||
|
||||
mockAuthNotifier.switchProfileCompleter!.complete();
|
||||
await tester.pumpAndSettle();
|
||||
@@ -193,14 +194,20 @@ void main() {
|
||||
|
||||
testWidgets('displays formatted npub for first account', (tester) async {
|
||||
await pumpSwitchProfileScreen(tester, testPubkeyA);
|
||||
expect(find.textContaining(testNpubAFormatted), findsOneWidget);
|
||||
final middleEllipsisWidgets = tester.widgetList<WnMiddleEllipsisText>(
|
||||
find.byType(WnMiddleEllipsisText),
|
||||
);
|
||||
expect(middleEllipsisWidgets.any((w) => w.text.startsWith('npub 1a1b')), isTrue);
|
||||
});
|
||||
|
||||
testWidgets('displays formatted npub for second account', (tester) async {
|
||||
await pumpSwitchProfileScreen(tester, testPubkeyA);
|
||||
await tester.drag(find.byType(ListView), const Offset(0, -400));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining(testNpubBFormatted), findsOneWidget);
|
||||
final middleEllipsisWidgets = tester.widgetList<WnMiddleEllipsisText>(
|
||||
find.byType(WnMiddleEllipsisText),
|
||||
);
|
||||
expect(middleEllipsisWidgets.any((w) => w.text.startsWith('npub 1b2c')), isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:whitenoise/widgets/wn_profile_switcher_item.dart';
|
||||
|
||||
import '../test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('WnProfileSwitcherItem', () {
|
||||
testWidgets('displays name and public key', (tester) async {
|
||||
await mountWidget(
|
||||
WnProfileSwitcherItem(
|
||||
pubkey: testPubkeyA,
|
||||
displayName: 'Alice',
|
||||
onTap: () {},
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
expect(find.text('Alice'), findsOneWidget);
|
||||
expect(find.byType(Text), findsAtLeast(2));
|
||||
});
|
||||
|
||||
testWidgets('displays avatar', (tester) async {
|
||||
await mountWidget(
|
||||
WnProfileSwitcherItem(
|
||||
pubkey: testPubkeyA,
|
||||
displayName: 'Alice',
|
||||
onTap: () {},
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
expect(find.text('A'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('displays checkmark when selected', (tester) async {
|
||||
await mountWidget(
|
||||
WnProfileSwitcherItem(
|
||||
pubkey: testPubkeyA,
|
||||
displayName: 'Alice',
|
||||
isSelected: true,
|
||||
onTap: () {},
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
expect(find.byKey(const Key('profile_switcher_item_checkmark')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('does not display checkmark when not selected', (tester) async {
|
||||
await mountWidget(
|
||||
WnProfileSwitcherItem(
|
||||
pubkey: testPubkeyA,
|
||||
displayName: 'Alice',
|
||||
onTap: () {},
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
expect(find.byKey(const Key('profile_switcher_item_checkmark')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('calls onTap when tapped', (tester) async {
|
||||
bool tapped = false;
|
||||
|
||||
await mountWidget(
|
||||
WnProfileSwitcherItem(
|
||||
pubkey: testPubkeyA,
|
||||
displayName: 'Alice',
|
||||
onTap: () => tapped = true,
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
await tester.tap(find.text('Alice'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tapped, true);
|
||||
});
|
||||
|
||||
testWidgets('displays formatted pubkey when no displayName', (tester) async {
|
||||
await mountWidget(
|
||||
WnProfileSwitcherItem(
|
||||
pubkey: testPubkeyA,
|
||||
onTap: () {},
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
expect(find.byType(Text), findsAtLeast(2));
|
||||
});
|
||||
|
||||
testWidgets('handles pictureUrl parameter', (tester) async {
|
||||
await mountWidget(
|
||||
WnProfileSwitcherItem(
|
||||
pubkey: testPubkeyA,
|
||||
displayName: 'Alice',
|
||||
pictureUrl: 'https://example.com/avatar.png',
|
||||
onTap: () {},
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
expect(find.text('Alice'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('applies background color when selected', (tester) async {
|
||||
await mountWidget(
|
||||
WnProfileSwitcherItem(
|
||||
pubkey: testPubkeyA,
|
||||
displayName: 'Alice',
|
||||
isSelected: true,
|
||||
onTap: () {},
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
final container = tester.widget<Container>(
|
||||
find
|
||||
.ancestor(
|
||||
of: find.text('Alice'),
|
||||
matching: find.byType(Container),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
|
||||
expect(container.decoration, isNotNull);
|
||||
});
|
||||
|
||||
testWidgets('renders with different pubkeys', (tester) async {
|
||||
await mountWidget(
|
||||
Column(
|
||||
children: [
|
||||
WnProfileSwitcherItem(
|
||||
pubkey: testPubkeyA,
|
||||
displayName: 'Alice',
|
||||
onTap: () {},
|
||||
),
|
||||
WnProfileSwitcherItem(
|
||||
pubkey: testPubkeyB,
|
||||
displayName: 'Bob',
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
expect(find.text('Alice'), findsOneWidget);
|
||||
expect(find.text('Bob'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('handles long display names with ellipsis', (tester) async {
|
||||
await mountWidget(
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: WnProfileSwitcherItem(
|
||||
pubkey: testPubkeyA,
|
||||
displayName: 'This is a very long display name that should be truncated',
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
tester,
|
||||
);
|
||||
|
||||
final textWidget = tester.widget<Text>(
|
||||
find.text(
|
||||
'This is a very long display name that should be truncated',
|
||||
),
|
||||
);
|
||||
expect(textWidget.overflow, TextOverflow.ellipsis);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:whitenoise/theme.dart';
|
||||
import 'package:whitenoise/widgets/wn_profile_switcher_item.dart';
|
||||
import 'package:widgetbook/widgetbook.dart';
|
||||
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
|
||||
|
||||
class WnProfileSwitcherItemStory extends StatelessWidget {
|
||||
const WnProfileSwitcherItemStory({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
const _samplePubkeyA =
|
||||
'npub1zuuajd7u3sx8xu92yav9jwxpr839cs0kc3q6t56vd5u9q033xmhsk6c2uc';
|
||||
const _samplePubkeyB =
|
||||
'npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6';
|
||||
const _samplePubkeyC =
|
||||
'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg';
|
||||
|
||||
@widgetbook.UseCase(
|
||||
name: 'Profile Switcher Item',
|
||||
type: WnProfileSwitcherItemStory,
|
||||
)
|
||||
Widget wnProfileSwitcherItemShowcase(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: context.colors.backgroundPrimary,
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
Text(
|
||||
'Playground',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.colors.backgroundContentPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Use the knobs panel to customize this profile switcher item.',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: context.colors.backgroundContentSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 375),
|
||||
child: _InteractiveProfileSwitcherItem(context: context),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Divider(color: context.colors.borderTertiary),
|
||||
const SizedBox(height: 24),
|
||||
_buildSection(
|
||||
context,
|
||||
'States',
|
||||
'Profile switcher item can be in different states: default or selected.',
|
||||
[
|
||||
_ProfileSwitcherItemExample(
|
||||
label: 'Default',
|
||||
child: WnProfileSwitcherItem(
|
||||
pubkey: _samplePubkeyA,
|
||||
displayName: 'Marina Hofmann',
|
||||
isSelected: false,
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
_ProfileSwitcherItemExample(
|
||||
label: 'Selected (Active)',
|
||||
child: WnProfileSwitcherItem(
|
||||
pubkey: _samplePubkeyA,
|
||||
displayName: 'Marina Hofmann',
|
||||
isSelected: true,
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
_buildSection(
|
||||
context,
|
||||
'Content Variations',
|
||||
'Profile switcher item with different content configurations.',
|
||||
[
|
||||
_ProfileSwitcherItemExample(
|
||||
label: 'With Display Name',
|
||||
child: WnProfileSwitcherItem(
|
||||
pubkey: _samplePubkeyA,
|
||||
displayName: 'Marina Hofmann',
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
_ProfileSwitcherItemExample(
|
||||
label: 'Without Display Name',
|
||||
child: WnProfileSwitcherItem(
|
||||
pubkey: _samplePubkeyA,
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
_ProfileSwitcherItemExample(
|
||||
label: 'With Long Name',
|
||||
child: WnProfileSwitcherItem(
|
||||
pubkey: _samplePubkeyB,
|
||||
displayName:
|
||||
'This Is A Very Long Display Name That Should Truncate',
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
_buildSection(
|
||||
context,
|
||||
'List Example',
|
||||
'Multiple profile switcher items in a list layout.',
|
||||
[_ProfileListExample()],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSection(
|
||||
BuildContext context,
|
||||
String title,
|
||||
String description,
|
||||
List<Widget> children,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: context.colors.backgroundContentPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
description,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: context.colors.backgroundContentSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(spacing: 24, runSpacing: 24, children: children),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _ProfileSwitcherItemExample extends StatelessWidget {
|
||||
const _ProfileSwitcherItemExample({required this.label, required this.child});
|
||||
|
||||
final String label;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 368,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: context.colors.backgroundContentSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProfileListExample extends StatefulWidget {
|
||||
@override
|
||||
State<_ProfileListExample> createState() => _ProfileListExampleState();
|
||||
}
|
||||
|
||||
class _ProfileListExampleState extends State<_ProfileListExample> {
|
||||
String _selectedPubkey = _samplePubkeyA;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 368,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Selectable List',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: context.colors.backgroundContentSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Column(
|
||||
children: [
|
||||
WnProfileSwitcherItem(
|
||||
pubkey: _samplePubkeyA,
|
||||
displayName: 'Marina Hofmann',
|
||||
isSelected: _selectedPubkey == _samplePubkeyA,
|
||||
onTap: () => setState(() => _selectedPubkey = _samplePubkeyA),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
WnProfileSwitcherItem(
|
||||
pubkey: _samplePubkeyB,
|
||||
displayName: 'Bob Smith',
|
||||
isSelected: _selectedPubkey == _samplePubkeyB,
|
||||
onTap: () => setState(() => _selectedPubkey = _samplePubkeyB),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
WnProfileSwitcherItem(
|
||||
pubkey: _samplePubkeyC,
|
||||
displayName: 'Charlie Brown',
|
||||
isSelected: _selectedPubkey == _samplePubkeyC,
|
||||
onTap: () => setState(() => _selectedPubkey = _samplePubkeyC),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InteractiveProfileSwitcherItem extends StatefulWidget {
|
||||
const _InteractiveProfileSwitcherItem({required this.context});
|
||||
|
||||
final BuildContext context;
|
||||
|
||||
@override
|
||||
State<_InteractiveProfileSwitcherItem> createState() =>
|
||||
_InteractiveProfileSwitcherItemState();
|
||||
}
|
||||
|
||||
class _InteractiveProfileSwitcherItemState
|
||||
extends State<_InteractiveProfileSwitcherItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final displayName = widget.context.knobs.stringOrNull(
|
||||
label: 'Display Name',
|
||||
initialValue: 'Marina Hofmann',
|
||||
);
|
||||
final isSelected = widget.context.knobs.boolean(
|
||||
label: 'Selected',
|
||||
initialValue: false,
|
||||
);
|
||||
|
||||
return WnProfileSwitcherItem(
|
||||
pubkey: _samplePubkeyA,
|
||||
displayName: displayName,
|
||||
isSelected: isSelected,
|
||||
onTap: () {},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,8 @@ import 'package:whitenoise_widgetbook/components/wn_avatar.dart'
|
||||
as _whitenoise_widgetbook_components_wn_avatar;
|
||||
import 'package:whitenoise_widgetbook/components/wn_copy_card.dart'
|
||||
as _whitenoise_widgetbook_components_wn_copy_card;
|
||||
import 'package:whitenoise_widgetbook/components/wn_profile_switcher_item.dart'
|
||||
as _whitenoise_widgetbook_components_wn_profile_switcher_item;
|
||||
import 'package:whitenoise_widgetbook/foundations/semantic_colors.dart'
|
||||
as _whitenoise_widgetbook_foundations_semantic_colors;
|
||||
import 'package:whitenoise_widgetbook/foundations/typography.dart'
|
||||
@@ -217,6 +219,16 @@ final directories = <_widgetbook.WidgetbookNode>[
|
||||
),
|
||||
],
|
||||
),
|
||||
_widgetbook.WidgetbookComponent(
|
||||
name: 'WnProfileSwitcherItemStory',
|
||||
useCases: [
|
||||
_widgetbook.WidgetbookUseCase(
|
||||
name: 'Profile Switcher Item',
|
||||
builder: _whitenoise_widgetbook_components_wn_profile_switcher_item
|
||||
.wnProfileSwitcherItemShowcase,
|
||||
),
|
||||
],
|
||||
),
|
||||
_widgetbook.WidgetbookComponent(
|
||||
name: 'WnScrollEdgeEffectStory',
|
||||
useCases: [
|
||||
|
||||
Reference in New Issue
Block a user