ui_framework_kit 1.1.5 copy "ui_framework_kit: ^1.1.5" to clipboard
ui_framework_kit: ^1.1.5 copied to clipboard

Platformweb

Production-ready Flutter UI kit: adaptive light/dark theming, form widgets, MCQ, shimmer skeletons, HTML/image renderers, and responsive utilities — all in one import.

example/lib/main.dart

import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'package:ui_framework_kit/ui_framework_kit.dart';

void main() {
  // Optional: configure a custom font before running the app.
  // UiFrameworkConfig.configure(fontFamily: 'Nunito');
  runApp(const ExampleApp());
}

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ScreenUtilInit(
      designSize: const Size(360, 690),
      minTextAdapt: true,
      splitScreenMode: true,
      builder: (_, __) => MaterialApp(
        title: 'ui_framework Example',
        debugShowCheckedModeBanner: false,
        theme: lightTheme,
        darkTheme: darkTheme,
        home: const _HomeScreen(),
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Home — navigation list
// ---------------------------------------------------------------------------

class _HomeScreen extends StatelessWidget {
  const _HomeScreen();

  @override
  Widget build(BuildContext context) {
    final sections = [
      ('Typography', Icons.text_fields_outlined, const _TypographyPage()),
      ('Buttons', Icons.smart_button_outlined, const _ButtonsPage()),
      ('Form Inputs', Icons.edit_outlined, const _FormPage()),
      ('Shimmer Skeletons', Icons.blur_on_outlined, const _ShimmerPage()),
      ('MCQ Widget', Icons.quiz_outlined, const _McqPage()),
      ('Images & Avatars', Icons.image_outlined, const _ImagePage()),
      ('Newsfeed / Author Tile', Icons.newspaper_outlined, const _NewsfeedPage()),
      ('Carousel', Icons.view_carousel_outlined, const _CarouselPage()),
      ('YouTube Player', Icons.play_circle_outline, const _YouTubePage()),
      ('Link Previewer', Icons.link_outlined, const _LinkPreviewPage()),
      ('Web View', Icons.public_outlined, const _WebViewPage()),
      ('HTML Rendering', Icons.code_outlined, const _HtmlPage()),
      ('Read More Text', Icons.article_outlined, const _ReadMorePage()),
      ('Theming & Colors', Icons.palette_outlined, const _ThemePage()),
    ];

    return Scaffold(
      appBar: AppBar(
        title: const TextWidget('ui_framework_kit', textType: TextTypeConstants.heading),
      ),
      body: ListView.separated(
        padding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 16.w),
        itemCount: sections.length,
        separatorBuilder: (_, __) => 8.verticalSpace,
        itemBuilder: (ctx, i) {
          final (label, icon, page) = sections[i];
          return ListTile(
            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.r)),
            tileColor: getColorByTheme(context: ctx, colorClass: AppColors.secondaryColor),
            leading: Icon(icon),
            title: TextWidget(label, textType: TextTypeConstants.subTitle),
            trailing: const Icon(Icons.chevron_right),
            onTap: () => Navigator.push(ctx, MaterialPageRoute(builder: (_) => page)),
          );
        },
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// 1. Typography
// ---------------------------------------------------------------------------

class _TypographyPage extends StatelessWidget {
  const _TypographyPage();

  @override
  Widget build(BuildContext context) {
    return _DemoPage(title: 'Typography', children: [
      _label('Display (w700, 20sp)'),
      const TextWidget('The quick brown fox', textType: TextTypeConstants.display),
      16.verticalSpace,
      _label('Heading (w600, 18sp)'),
      const TextWidget('The quick brown fox', textType: TextTypeConstants.heading),
      16.verticalSpace,
      _label('Title (w500, 16sp)'),
      const TextWidget('The quick brown fox', textType: TextTypeConstants.title),
      16.verticalSpace,
      _label('SubTitle (w500, 14sp)'),
      const TextWidget('The quick brown fox', textType: TextTypeConstants.subTitle),
      16.verticalSpace,
      _label('Normal (w400, 12sp)'),
      const TextWidget('The quick brown fox', textType: TextTypeConstants.normal),
      16.verticalSpace,
      _label('Custom colour, weight, letter-spacing'),
      TextWidget(
        'Custom styled text',
        textType: TextTypeConstants.title,
        color: Colors.deepPurple,
        fontWeight: FontWeight.w800,
        textOptions: const TextOptions(letterSpacing: 1.5),
      ),
      16.verticalSpace,
      _label('Overflow / maxLines = 2'),
      const TextWidget(
        'This is a very long piece of text that should be clamped to two lines '
        'because maxLines is set to 2 and overflow is set to ellipsis. '
        'More text here to force truncation.',
        textType: TextTypeConstants.normal,
        maxLines: 2,
        overflow: TextOverflow.ellipsis,
      ),
    ]);
  }
}

// ---------------------------------------------------------------------------
// 2. Buttons
// ---------------------------------------------------------------------------

class _ButtonsPage extends StatelessWidget {
  const _ButtonsPage();

  @override
  Widget build(BuildContext context) {
    return _DemoPage(title: 'Buttons', children: [
      _label('Filled button'),
      InputButtonWidget(title: 'Submit', onTap: () {}, bgColor: AppColors.blueColor),
      16.verticalSpace,
      _label('Border button'),
      InputButtonWidget(
          title: 'Cancel', isBorderButton: true, bgColor: AppColors.blueColor, onTap: () {}),
      16.verticalSpace,
      _label('Success / Error variants'),
      Row(
        children: [
          Expanded(
              child: InputButtonWidget(
                  title: 'Approve', bgColor: AppColors.successColor, onTap: () {})),
          8.horizontalSpace,
          Expanded(
              child: InputButtonWidget(
                  title: 'Reject',
                  isBorderButton: true,
                  bgColor: AppColors.errorColor,
                  onTap: () {})),
        ],
      ),
      16.verticalSpace,
      _label('Loading state'),
      const InputButtonWidget(title: 'Saving…', isLoading: true),
      16.verticalSpace,
      _label('Disabled (no onTap)'),
      const InputButtonWidget(title: 'Disabled'),
      16.verticalSpace,
      _label('Debounce demo — tap rapidly, fires once'),
      InputButtonWidget(
        title: 'Debounced (2 s)',
        bgColor: AppColors.pendingColor,
        debounceDuration: const Duration(seconds: 2),
        onTap: () {},
      ),
    ]);
  }
}

// ---------------------------------------------------------------------------
// 3. Form Inputs
// ---------------------------------------------------------------------------

class _FormPage extends StatefulWidget {
  const _FormPage();

  @override
  State<_FormPage> createState() => _FormPageState();
}

class _FormPageState extends State<_FormPage> {
  final _formKey = GlobalKey<FormState>();
  String? _selectedCountry;
  final List<String> _countries = ['Nepal', 'India', 'USA', 'UK', 'Germany', 'Japan'];

  @override
  Widget build(BuildContext context) {
    return _DemoPage(
      title: 'Form Inputs',
      children: [
        Form(
          key: _formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              _label('TextFieldWidget — plain text'),
              TextFieldWidget(
                labelText: 'Full Name',
                hintText: 'Enter your full name',
                validator: (v) => (v == null || v.isEmpty) ? 'Required' : null,
              ),
              16.verticalSpace,
              _label('Password field (toggleable visibility)'),
              const TextFieldWidget(
                labelText: 'Password',
                hintText: '••••••••',
                isPassword: true,
              ),
              16.verticalSpace,
              _label('Multiline / TextArea'),
              const TextFieldWidget(
                labelText: 'Bio',
                hintText: 'Tell us about yourself…',
                maxLines: 4,
                minLines: 3,
              ),
              16.verticalSpace,
              _label('DatePickerTextField — built-in Gregorian'),
              DatePickerTextField(
                labelText: 'Date of Birth',
                firstDate: DateTime(1940),
                lastDate: DateTime.now(),
                onChanged: (_) {},
              ),
              16.verticalSpace,
              _label('DatePickerTextField — custom dd/MM/yyyy format'),
              DatePickerTextField(
                labelText: 'Event Date',
                dateFormatter: (d) =>
                    '${d.day.toString().padLeft(2, '0')}/'
                    '${d.month.toString().padLeft(2, '0')}/'
                    '${d.year}',
                onChanged: (_) {},
              ),
              16.verticalSpace,
              _label('TimePickerTextField'),
              TimePickerTextField(
                labelText: 'Appointment Time',
                onChanged: (_) {},
              ),
              16.verticalSpace,
              _label('DropdownWidget<String>'),
              DropdownWidget<String>(
                hintText: 'Select Country',
                itemList: _countries,
                initialSelectedValue: _selectedCountry,
                showSelected: true,
                onPressed: (v) => setState(() => _selectedCountry = v),
              ),
              16.verticalSpace,
              _label('CheckBoxWidget (with label)'),
              CheckBoxWidget(title: 'Accept terms and conditions', onChanged: (_) {}),
              16.verticalSpace,
              _label('SwitchWidget'),
              SwitchWidget(title: 'Enable notifications', onSelected: (_) {}),
              24.verticalSpace,
              InputButtonWidget(
                title: 'Validate',
                onTap: () {
                  if (_formKey.currentState!.validate()) {
                    context.showToastMessage(
                        toastEnum: ToastEnum.success, toastMessage: 'Form is valid!');
                  }
                },
              ),
            ],
          ),
        ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// 4. Shimmer Skeletons
// ---------------------------------------------------------------------------

class _ShimmerPage extends StatelessWidget {
  const _ShimmerPage();

  @override
  Widget build(BuildContext context) {
    return _DemoPage(
      title: 'Shimmer Skeletons',
      children: [
        for (final type in ShimmerType.values) ...[
          _label(type.name),
          ShimmerWidget(shimmerType: type),
          16.verticalSpace,
        ],
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// 5. MCQ Widget
// ---------------------------------------------------------------------------

class _McqPage extends StatefulWidget {
  const _McqPage();

  @override
  State<_McqPage> createState() => _McqPageState();
}

class _McqPageState extends State<_McqPage> {
  late MCQConfig _config;

  @override
  void initState() {
    super.initState();
    _reset();
  }

  void _reset() {
    _config = MCQConfig(
      id: 'q1',
      questionNoToDisplay: 1,
      question: 'Which of the following is a <b>compiled</b> language?',
      answers: [
        MCQAnswerConfig(id: 'a', answer: 'Python'),
        MCQAnswerConfig(id: 'b', answer: 'JavaScript'),
        MCQAnswerConfig(id: 'c', answer: 'Go'),
        MCQAnswerConfig(id: 'd', answer: 'Ruby'),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return _DemoPage(title: 'MCQ Widget', children: [
      McqWidget(
        mcqConfig: _config,
        selectedAnswerCallback: (questionId, answer, index) {
          setState(() {
            _config = MCQConfig(
              id: _config.id,
              questionNoToDisplay: _config.questionNoToDisplay,
              question: _config.question,
              answers: _config.answers.asMap().entries.map((e) {
                final isCorrect = e.key == 2; // 'Go' is correct
                return e.value.copyWith(
                  isSelected: e.key == index,
                  isCorrectAnswer: e.key == index && isCorrect,
                  isIncorrectAnswer: e.key == index && !isCorrect,
                );
              }).toList(),
              isToSelectAnswer: false,
            );
          });
        },
      ),
      16.verticalSpace,
      TextWidget(
        'Tap an answer.  Green = correct, Red = incorrect.',
        textType: TextTypeConstants.normal,
        color: getColorByTheme(context: context, colorClass: AppColors.greyColor),
      ),
      12.verticalSpace,
      InputButtonWidget(
          title: 'Reset',
          isBorderButton: true,
          bgColor: AppColors.greyColor,
          onTap: () => setState(_reset)),
    ]);
  }
}

// ---------------------------------------------------------------------------
// 6. Images & Avatars
// ---------------------------------------------------------------------------

class _ImagePage extends StatelessWidget {
  const _ImagePage();

  static const _img =
      'https://images.unsplash.com/photo-1501854140801-50d01698950b?w=640&q=80';

  @override
  Widget build(BuildContext context) {
    return _DemoPage(title: 'Images & Avatars', children: [
      _label('CachedNetworkImageWidget'),
      CachedNetworkImageWidget(imageUrl: _img, height: 180.h, width: double.infinity),
      16.verticalSpace,
      _label('CircularCachedNetworkImage — loaded & fallback'),
      Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          Column(children: [
            CircularCachedNetworkImage(imageUrl: _img, size: 64.w, fallBackString: 'Alice'),
            4.verticalSpace,
            const TextWidget('loaded', textType: TextTypeConstants.normal),
          ]),
          Column(children: [
            CircularCachedNetworkImage(
                imageUrl: 'https://invalid/404.png', size: 64.w, fallBackString: 'Bob Smith'),
            4.verticalSpace,
            const TextWidget('initials fallback', textType: TextTypeConstants.normal),
          ]),
          Column(children: [
            CircularCachedNetworkImage(imageUrl: '', size: 64.w, fallBackString: 'C'),
            4.verticalSpace,
            const TextWidget('single letter', textType: TextTypeConstants.normal),
          ]),
        ],
      ),
    ]);
  }
}

// ---------------------------------------------------------------------------
// 7. Newsfeed / Author Tile — multiple use-cases
// ---------------------------------------------------------------------------

class _NewsfeedPage extends StatelessWidget {
  const _NewsfeedPage();

  static const _avatar  = 'https://i.pravatar.cc/150?img=12';
  static const _avatar2 = 'https://i.pravatar.cc/150?img=47';
  static const _avatar3 = 'https://i.pravatar.cc/150?img=33';
  static const _avatar4 = 'https://i.pravatar.cc/150?img=68';

  // Landscape photos for posts
  static const _photo1 =
      'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=900&q=80';
  static const _photo2 =
      'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=900&q=80';
  static const _photo3 =
      'https://images.unsplash.com/photo-1551632811-561732d1e306?w=900&q=80';
  static const _photo4 =
      'https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?w=900&q=80';
  static const _photo5 =
      'https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=900&q=80';

  // ── helpers ───────────────────────────────────────────────────────────────

  Widget _postHeader(
    BuildContext context, {
    required String avatarUrl,
    required String name,
    required String timeAgo,
    bool verified = false,
  }) {
    return AuthorListTileWidget(
      authorListTileModel: AuthorListTileModel(
        authorImageConfig: AuthorImageConfig(
          imageUrl: avatarUrl,
          isToDisplayAuthor: true,
          imageSize: 42.w,
        ),
        titleConfig: AuthorTitleConfig(
          title: name,
          titleFontWeight: FontWeight.w700,
          suffixWidget: verified
              ? const Icon(Icons.verified, color: Colors.blue, size: 15)
              : null,
        ),
        descConfig: AuthorTitleConfig(
          title: timeAgo,
          titleFontColor: AppColors.greyColor,
        ),
        suffixWidget: IconButton(
          icon: const Icon(Icons.more_horiz),
          onPressed: () {},
        ),
      ),
    );
  }

  Widget _actionBar(
    BuildContext context, {
    required int likes,
    required int comments,
  }) {
    final grey = getColorByTheme(context: context, colorClass: AppColors.greyColor);
    return Padding(
      padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
      child: Row(
        children: [
          _ActionChip(
            icon: Icons.favorite_border_rounded,
            label: '$likes',
            color: grey,
          ),
          12.horizontalSpace,
          _ActionChip(
            icon: Icons.chat_bubble_outline_rounded,
            label: '$comments',
            color: grey,
          ),
          12.horizontalSpace,
          _ActionChip(
            icon: Icons.share_outlined,
            label: 'Share',
            color: grey,
          ),
          const Spacer(),
          Icon(Icons.bookmark_border_rounded, size: 20.w, color: grey),
        ],
      ),
    );
  }

  Widget _postCard(BuildContext context, {required List<Widget> children}) {
    return Card(
      margin: EdgeInsets.only(bottom: 16.h),
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12.r),
        side: BorderSide(
          color: getColorByTheme(context: context, colorClass: AppColors.borderColor),
        ),
      ),
      clipBehavior: Clip.antiAlias,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: children,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return _DemoPage(
      title: 'Newsfeed / Author Tile',
      children: [
        // ── Use case 1: Social post — text only with action bar ──────────
        _label('Text post'),
        _postCard(context, children: [
          _postHeader(context,
              avatarUrl: _avatar,
              name: 'Aarav Sharma',
              timeAgo: '2 hours ago · Public',
              verified: true),
          Padding(
            padding: EdgeInsets.fromLTRB(12.w, 0, 12.w, 4.h),
            child: const TextWidget(
              'Just shipped a new Flutter UI framework to pub.dev! '
              'Check it out and let me know what you think.',
              textType: TextTypeConstants.subTitle,
            ),
          ),
          _actionBar(context, likes: 128, comments: 24),
        ]),

        // ── Use case 2: Post with single full-width image ────────────────
        _label('Post with single image'),
        _postCard(context, children: [
          _postHeader(context,
              avatarUrl: _avatar2,
              name: 'Priya Patel',
              timeAgo: '5 hours ago'),
          Padding(
            padding: EdgeInsets.fromLTRB(12.w, 0, 12.w, 8.h),
            child: const TextWidget(
              'Sunrise from the summit. Worth every step.',
              textType: TextTypeConstants.subTitle,
            ),
          ),
          AspectRatio(
            aspectRatio: 16 / 9,
            child: CachedNetworkImageWidget(
              imageUrl: _photo1,
              fit: BoxFit.cover,
            ),
          ),
          _actionBar(context, likes: 342, comments: 51),
        ]),

        // ── Use case 3: Post with 2 images side-by-side ──────────────────
        _label('Post with 2 images'),
        _postCard(context, children: [
          _postHeader(context,
              avatarUrl: _avatar3,
              name: 'Rajan Thapa',
              timeAgo: 'Yesterday'),
          Padding(
            padding: EdgeInsets.fromLTRB(12.w, 0, 12.w, 8.h),
            child: const TextWidget(
              'Weekend hike — the views were breathtaking!',
              textType: TextTypeConstants.subTitle,
            ),
          ),
          SizedBox(
            height: 180.h,
            child: Row(
              children: [
                Expanded(
                  child: CachedNetworkImageWidget(
                    imageUrl: _photo2,
                    fit: BoxFit.cover,
                  ),
                ),
                2.horizontalSpace,
                Expanded(
                  child: CachedNetworkImageWidget(
                    imageUrl: _photo3,
                    fit: BoxFit.cover,
                  ),
                ),
              ],
            ),
          ),
          _actionBar(context, likes: 64, comments: 8),
        ]),

        // ── Use case 4: Post with image mosaic (3 + overflow badge) ──────
        _label('Post with 4-image mosaic'),
        _postCard(context, children: [
          _postHeader(context,
              avatarUrl: _avatar4,
              name: 'Sunita Rai',
              timeAgo: '2 days ago'),
          Padding(
            padding: EdgeInsets.fromLTRB(12.w, 0, 12.w, 8.h),
            child: const TextWidget(
              'Photo dump from the Flutter conference.',
              textType: TextTypeConstants.subTitle,
            ),
          ),
          SizedBox(
            height: 200.h,
            child: Row(
              children: [
                // large left image
                Expanded(
                  flex: 2,
                  child: CachedNetworkImageWidget(
                    imageUrl: _photo1,
                    fit: BoxFit.cover,
                    height: 200.h,
                  ),
                ),
                2.horizontalSpace,
                // two stacked right images
                Expanded(
                  child: Column(
                    children: [
                      Expanded(
                        child: CachedNetworkImageWidget(
                          imageUrl: _photo4,
                          fit: BoxFit.cover,
                          width: double.infinity,
                        ),
                      ),
                      2.verticalSpace,
                      Expanded(
                        child: Stack(
                          fit: StackFit.expand,
                          children: [
                            CachedNetworkImageWidget(
                              imageUrl: _photo5,
                              fit: BoxFit.cover,
                            ),
                            // "+2 more" overlay on the last tile
                            Container(
                              color: Colors.black54,
                              alignment: Alignment.center,
                              child: const TextWidget(
                                '+2',
                                textType: TextTypeConstants.title,
                                color: Colors.white,
                              ),
                            ),
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
          _actionBar(context, likes: 203, comments: 47),
        ]),

        // ── Use case 5: Post with link preview ───────────────────────────
        _label('Post with link preview'),
        _postCard(context, children: [
          _postHeader(context,
              avatarUrl: _avatar,
              name: 'Aarav Sharma',
              timeAgo: '3 days ago',
              verified: true),
          Padding(
            padding: EdgeInsets.fromLTRB(12.w, 0, 12.w, 8.h),
            child: const TextWidget(
              'This article on Dart 3 macros is a must-read for every Flutter dev.',
              textType: TextTypeConstants.subTitle,
            ),
          ),
          Padding(
            padding: EdgeInsets.symmetric(horizontal: 12.w),
            child: const LinkPreviewWidget(
              link: 'https://dart.dev/language/macros',
            ),
          ),
          _actionBar(context, likes: 56, comments: 12),
        ]),

        // ── Use case 6: News article list item ───────────────────────────
        _sectionDivider(),
        _label('News article list item'),
        AuthorListTileWidget(
          authorListTileModel: AuthorListTileModel(
            authorDecorationConfig: AuthorDecorationConfig(isBorderRequired: true),
            authorImageConfig: AuthorImageConfig(
              imageUrl: 'https://i.pravatar.cc/150?img=5',
              isToDisplayAuthor: true,
              imageSize: 40.w,
            ),
            titleConfig: AuthorTitleConfig(
              title: 'Flutter Weekly Newsletter',
              titleFontSize: 13,
              titleFontWeight: FontWeight.w600,
            ),
            descConfig: AuthorTitleConfig(
              title: "What's new in Flutter 3.29 — deep dive into Impeller and "
                  'the new animation framework.',
              maxLines: 2,
            ),
            suffixWidget: Container(
              padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
              decoration: BoxDecoration(
                color: getColorByTheme(
                        context: context, colorClass: AppColors.blueColor)
                    .withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(4.r),
              ),
              child: TextWidget(
                'Tech',
                textType: TextTypeConstants.normal,
                color: getColorByTheme(
                    context: context, colorClass: AppColors.blueColor),
              ),
            ),
          ),
        ),
        12.verticalSpace,

        // ── Use case 7: Comment row ──────────────────────────────────────
        _sectionDivider(),
        _label('Comment row'),
        AuthorListTileWidget(
          authorListTileModel: AuthorListTileModel(
            authorImageConfig: AuthorImageConfig(
              imageUrl: _avatar2,
              isToDisplayAuthor: true,
              imageSize: 36.w,
            ),
            titleConfig: AuthorTitleConfig(
              title: 'Priya Patel',
              titleFontWeight: FontWeight.w600,
              titleFontSize: 13,
            ),
            descConfig: AuthorTitleConfig(
              title: "Amazing work! I've been waiting for this. The MCQ widget alone is worth it.",
              maxLines: 3,
            ),
            suffixWidget: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const Icon(Icons.thumb_up_alt_outlined, size: 16),
                4.verticalSpace,
                const TextWidget('24', textType: TextTypeConstants.normal),
              ],
            ),
          ),
        ),
        12.verticalSpace,

        // ── Use case 8: Contact list ─────────────────────────────────────
        _sectionDivider(),
        _label('Contact list'),
        ...['Alice Johnson', 'Bob Marley', 'Carol White'].asMap().entries.map((e) {
          return Column(
            children: [
              AuthorListTileWidget(
                authorListTileModel: AuthorListTileModel(
                  authorImageConfig: AuthorImageConfig(
                    imageUrl: 'https://i.pravatar.cc/150?img=${10 + e.key}',
                    isToDisplayAuthor: true,
                    imageSize: 40.w,
                    removeAuthorImagePadding: true,
                  ),
                  titleConfig: AuthorTitleConfig(
                    title: e.value,
                    titleFontWeight: FontWeight.w600,
                  ),
                  descConfig: AuthorTitleConfig(title: '+1 555-010${e.key}'),
                  suffixWidget: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      IconButton(
                          icon: const Icon(Icons.call_outlined, size: 20),
                          onPressed: () {}),
                      IconButton(
                          icon: const Icon(Icons.message_outlined, size: 20),
                          onPressed: () {}),
                    ],
                  ),
                ),
              ),
              DividerWidget(),
            ],
          );
        }),
        12.verticalSpace,

        // ── Use case 9: Chat list ────────────────────────────────────────
        _sectionDivider(),
        _label('Chat list'),
        ...{
          _avatar:  ('Aarav Sharma',  'Sure! See you tomorrow', '2m ago',  3),
          _avatar2: ('Priya Patel',   'Can you review my PR?',  '1h ago',  0),
          _avatar3: ('Rajan Thapa',   'Sent you the design files', '3h ago', 1),
        }.entries.map((e) {
          final (name, lastMsg, time, unread) = e.value;
          return Column(
            children: [
              AuthorListTileWidget(
                authorListTileModel: AuthorListTileModel(
                  authorImageConfig: AuthorImageConfig(
                    imageUrl: e.key,
                    isToDisplayAuthor: true,
                    imageSize: 44.w,
                    removeAuthorImagePadding: true,
                  ),
                  titleConfig: AuthorTitleConfig(
                    title: name,
                    titleFontWeight: FontWeight.w600,
                    suffixWidget: TextWidget(
                      time,
                      textType: TextTypeConstants.normal,
                      color: getColorByTheme(
                          context: context, colorClass: AppColors.greyColor),
                    ),
                  ),
                  descConfig: AuthorTitleConfig(
                    title: lastMsg,
                    titleFontColor: AppColors.greyColor,
                    maxLines: 1,
                  ),
                  suffixWidget: unread > 0
                      ? Container(
                          margin: EdgeInsets.only(right: 4.w),
                          padding: EdgeInsets.symmetric(
                              horizontal: 7.w, vertical: 3.h),
                          decoration: BoxDecoration(
                            color: getColorByTheme(
                                context: context,
                                colorClass: AppColors.blueColor),
                            borderRadius: BorderRadius.circular(20.r),
                          ),
                          child: TextWidget(
                            '$unread',
                            textType: TextTypeConstants.normal,
                            color: Colors.white,
                          ),
                        )
                      : null,
                ),
              ),
              DividerWidget(),
            ],
          );
        }),
      ],
    );
  }
}

class _ActionChip extends StatelessWidget {
  final IconData icon;
  final String label;
  final Color color;

  const _ActionChip({
    required this.icon,
    required this.label,
    required this.color,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {},
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(icon, size: 18.w, color: color),
          4.horizontalSpace,
          TextWidget(label, textType: TextTypeConstants.normal, color: color),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// 8. Carousel
// ---------------------------------------------------------------------------

class _CarouselPage extends StatefulWidget {
  const _CarouselPage();

  @override
  State<_CarouselPage> createState() => _CarouselPageState();
}

class _CarouselPageState extends State<_CarouselPage> {
  int _currentIndex = 0;

  static const _banners = [
    (
      'https://images.unsplash.com/photo-1517816743773-6e0fd518b4a6?w=800&q=80',
      'Mountain Sunrise',
      'Nepal Himalayas'
    ),
    (
      'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&q=80',
      'Alpine Lake',
      'Swiss Alps'
    ),
    (
      'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800&q=80',
      'Golden Valley',
      'Yosemite, USA'
    ),
  ];

  @override
  Widget build(BuildContext context) {
    return _DemoPage(title: 'Carousel', children: [
      _label('Image carousel with indicators'),
      CarouselSlider(
        options: CarouselOptions(
          height: 200.h,
          autoPlay: true,
          autoPlayInterval: const Duration(seconds: 3),
          enlargeCenterPage: true,
          viewportFraction: 0.9,
          onPageChanged: (index, _) => setState(() => _currentIndex = index),
        ),
        items: _banners.map((b) {
          final (url, title, sub) = b;
          return ClipRRect(
            borderRadius: BorderRadius.circular(12.r),
            child: Stack(
              fit: StackFit.expand,
              children: [
                CachedNetworkImageWidget(imageUrl: url, fit: BoxFit.cover),
                Positioned(
                  bottom: 0,
                  left: 0,
                  right: 0,
                  child: Container(
                    padding: EdgeInsets.all(12.w),
                    decoration: BoxDecoration(
                      gradient: LinearGradient(
                        begin: Alignment.bottomCenter,
                        end: Alignment.topCenter,
                        colors: [
                          Colors.black.withValues(alpha: 0.7),
                          Colors.transparent
                        ],
                      ),
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        TextWidget(title,
                            textType: TextTypeConstants.title,
                            color: Colors.white,
                            fontWeight: FontWeight.w700),
                        TextWidget(sub,
                            textType: TextTypeConstants.normal, color: Colors.white70),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          );
        }).toList(),
      ),
      12.verticalSpace,
      // Dot indicators
      Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: _banners.asMap().entries.map((e) {
          return AnimatedContainer(
            duration: const Duration(milliseconds: 300),
            margin: EdgeInsets.symmetric(horizontal: 3.w),
            width: _currentIndex == e.key ? 20.w : 8.w,
            height: 8.w,
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(4.r),
              color: _currentIndex == e.key
                  ? getColorByTheme(context: context, colorClass: AppColors.blueColor)
                  : getColorByTheme(context: context, colorClass: AppColors.greyColor),
            ),
          );
        }).toList(),
      ),
      24.verticalSpace,
      _label('Card carousel (viewportFraction = 0.75)'),
      CarouselSlider(
        options: CarouselOptions(
          height: 120.h,
          viewportFraction: 0.75,
          enableInfiniteScroll: true,
        ),
        items: [
          ('Breaking News', 'Flutter 4.0 lands with Material You'),
          ('Tutorial', 'Build a production MCQ app in Flutter'),
          ('Deep Dive', 'Riverpod vs BLoC — 2025 comparison'),
        ].map((item) {
          final (tag, headline) = item;
          return Container(
            margin: EdgeInsets.symmetric(horizontal: 6.w),
            padding: EdgeInsets.all(14.w),
            decoration: BoxDecoration(
              color: getColorByTheme(context: context, colorClass: AppColors.cardColor),
              borderRadius: BorderRadius.circular(10.r),
              border: Border.all(
                color: getColorByTheme(context: context, colorClass: AppColors.borderColor),
              ),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Container(
                  padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h),
                  decoration: BoxDecoration(
                    color: getColorByTheme(context: context, colorClass: AppColors.blueColor)
                        .withValues(alpha: 0.12),
                    borderRadius: BorderRadius.circular(4.r),
                  ),
                  child: TextWidget(tag,
                      textType: TextTypeConstants.normal,
                      color: getColorByTheme(context: context, colorClass: AppColors.blueColor)),
                ),
                8.verticalSpace,
                TextWidget(headline,
                    textType: TextTypeConstants.subTitle,
                    fontWeight: FontWeight.w600,
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis),
              ],
            ),
          );
        }).toList(),
      ),
    ]);
  }
}

// ---------------------------------------------------------------------------
// 9. YouTube Player
// ---------------------------------------------------------------------------

class _YouTubePage extends StatelessWidget {
  const _YouTubePage();

  static const _videos = [
    ('Flutter Intro (Google)', 'https://www.youtube.com/watch?v=fq4N0hgOWzU'),
    ('Dart in 100 Seconds', 'https://www.youtube.com/watch?v=NrO0CJCbYLA'),
    ('Flutter Riverpod Deep Dive', 'https://www.youtube.com/watch?v=yMgtTtDQoEA'),
  ];

  @override
  Widget build(BuildContext context) {
    return _DemoPage(title: 'YouTube Player', children: [
      for (final v in _videos) ...[
        _label(v.$1),
        ClipRRect(
          borderRadius: BorderRadius.circular(8.r),
          child: YoutubePlayerWidget(
            youtubePlayerConfig: YoutubePlayerConfig(youtubeUrl: v.$2),
          ),
        ),
        16.verticalSpace,
      ],
    ]);
  }
}

// ---------------------------------------------------------------------------
// 10. Link Previewer
// ---------------------------------------------------------------------------

class _LinkPreviewPage extends StatelessWidget {
  const _LinkPreviewPage();

  static const _links = [
    'https://flutter.dev',
    'https://pub.dev/packages/flutter_screenutil',
    'https://dart.dev',
  ];

  @override
  Widget build(BuildContext context) {
    return _DemoPage(title: 'Link Previewer', children: [
      for (final link in _links) ...[
        _label(link),
        Container(
          decoration: BoxDecoration(
            border: Border.all(
                color: getColorByTheme(context: context, colorClass: AppColors.borderColor)),
            borderRadius: BorderRadius.circular(8.r),
          ),
          padding: EdgeInsets.all(8.w),
          child: LinkPreviewWidget(link: link, width: double.infinity),
        ),
        16.verticalSpace,
      ],
    ]);
  }
}

// ---------------------------------------------------------------------------
// 11. Web View
// ---------------------------------------------------------------------------

class _WebViewPage extends StatelessWidget {
  const _WebViewPage();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const TextWidget('Web View', textType: TextTypeConstants.heading),
      ),
      body: WebViewWidget(
        url: 'https://flutter.dev',
        showProgressBar: true,
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// 12. HTML Rendering
// ---------------------------------------------------------------------------

class _HtmlPage extends StatelessWidget {
  const _HtmlPage();

  static const _html = '''
<h2>Rich HTML Content</h2>
<p>The <strong>RenderHtmlWidget</strong> renders <em>formatted</em> HTML inside Flutter:</p>
<ul>
  <li><b>Bold text</b> and <i>italic text</i></li>
  <li>Inline <code>code snippets</code></li>
  <li>Ordered &amp; unordered lists</li>
</ul>
<ol>
  <li>First numbered item</li>
  <li>Second numbered item</li>
  <li>Third numbered item</li>
</ol>
<p>Block quote example:</p>
<blockquote>
  "Code is like humor. When you have to explain it, it's bad." — Cory House
</blockquote>
<p>Visit <a href="https://flutter.dev">Flutter.dev</a> for the official docs.</p>
''';

  @override
  Widget build(BuildContext context) {
    return _DemoPage(title: 'HTML Rendering', children: [
      _label('RenderHtmlWidget'),
      RenderHtmlWidget(htmlContent: _html),
      24.verticalSpace,
      _label('Inline HTML inside MCQ answers'),
      McqWidget(
        mcqConfig: MCQConfig(
          id: 'html_q',
          questionNoToDisplay: 1,
          question:
              'The value of <code>2<sup>8</sup></code> is:',
          answers: [
            MCQAnswerConfig(id: 'a', answer: '128'),
            MCQAnswerConfig(id: 'b', answer: '<b>256</b>', isCorrectAnswer: true),
            MCQAnswerConfig(id: 'c', answer: '512'),
          ],
        ),
        selectedAnswerCallback: (_, __, ___) {},
      ),
    ]);
  }
}

// ---------------------------------------------------------------------------
// 13. Read More Text
// ---------------------------------------------------------------------------

class _ReadMorePage extends StatelessWidget {
  const _ReadMorePage();

  static const _longText = "Flutter is Google's UI toolkit for building "
      'beautiful, natively compiled applications for mobile, web, desktop, '
      'and embedded devices from a single codebase. Flutter works with '
      'existing code, is used by developers and organizations around the '
      'world, and is free and open source. It was first described in 2015 '
      'and was released in version 1.0 on December 4, 2018. Flutter is used '
      'internally by Google in multiple products, including Google Pay, '
      'and it has been growing in popularity since its original release.';

  @override
  Widget build(BuildContext context) {
    return _DemoPage(title: 'Read More Text', children: [
      _label('Trim by length (240 chars)'),
      ReadMoreText(
        _longText,
        trimMode: TrimMode.Length,
        trimLength: 120,
        colorClickableText:
            getColorByTheme(context: context, colorClass: AppColors.blueColor),
        style: context.textTheme.bodyMedium,
      ),
      24.verticalSpace,
      _label('Trim by lines (2 lines)'),
      ReadMoreText(
        _longText,
        trimMode: TrimMode.Line,
        trimLines: 2,
        colorClickableText:
            getColorByTheme(context: context, colorClass: AppColors.blueColor),
        style: context.textTheme.bodyMedium,
      ),
    ]);
  }
}

// ---------------------------------------------------------------------------
// 15. Theming & Colors
// ---------------------------------------------------------------------------

class _ThemePage extends StatelessWidget {
  const _ThemePage();

  static final _palette = <(String, ColorModel)>[
    ('primaryColor', AppColors.primaryColor),
    ('secondaryColor', AppColors.secondaryColor),
    ('blueColor', AppColors.blueColor),
    ('successColor', AppColors.successColor),
    ('errorColor', AppColors.errorColor),
    ('warningColor', AppColors.warningColor),
    ('pendingColor', AppColors.pendingColor),
    ('greyColor', AppColors.greyColor),
    ('cardColor', AppColors.cardColor),
    ('backgroundColor', AppColors.backgroundColor),
    ('hrTitleColor', AppColors.hrTitileColor),
    ('infoColor', AppColors.infoColor),
  ];

  @override
  Widget build(BuildContext context) {
    return _DemoPage(title: 'Theming & Colors', children: [
      _label('Adaptive color swatches (light ↔ dark)'),
      ..._palette.map((entry) {
        final (name, model) = entry;
        final resolved = getColorByTheme(context: context, colorClass: model);
        return Padding(
          padding: EdgeInsets.only(bottom: 8.h),
          child: Row(
            children: [
              Container(
                width: 40.w,
                height: 40.w,
                decoration: BoxDecoration(
                  color: resolved,
                  border: Border.all(
                      color: getColorByTheme(
                          context: context, colorClass: AppColors.borderColor)),
                  borderRadius: BorderRadius.circular(6.r),
                ),
              ),
              12.horizontalSpace,
              Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  TextWidget(name, textType: TextTypeConstants.subTitle),
                  TextWidget(
                    '#${resolved.toARGB32().toRadixString(16).padLeft(8, '0').toUpperCase()}',
                    textType: TextTypeConstants.normal,
                    color: getColorByTheme(context: context, colorClass: AppColors.greyColor),
                  ),
                ],
              ),
            ],
          ),
        );
      }),
      24.verticalSpace,
      _label('Toast notifications'),
      Wrap(
        spacing: 8.w,
        runSpacing: 8.h,
        children: [
          for (final t in ToastEnum.values)
            InputButtonWidget(
              title: t.name,
              bgColor: switch (t) {
                ToastEnum.success => AppColors.successColor,
                ToastEnum.error => AppColors.errorColor,
                ToastEnum.warning => AppColors.warningColor,
                ToastEnum.info => AppColors.infoColor,
              },
              onTap: () =>
                  context.showToastMessage(toastEnum: t, toastMessage: '${t.name} toast'),
            ),
        ],
      ),
    ]);
  }
}

// ---------------------------------------------------------------------------
// Shared helpers
// ---------------------------------------------------------------------------

class _DemoPage extends StatelessWidget {
  final String title;
  final List<Widget> children;

  const _DemoPage({required this.title, required this.children});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: TextWidget(title, textType: TextTypeConstants.heading),
      ),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(16.w),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: children,
        ),
      ),
    );
  }
}

Widget _label(String text) => Padding(
      padding: EdgeInsets.only(bottom: 6.h),
      child: TextWidget(
        text,
        textType: TextTypeConstants.normal,
        color: Colors.grey,
        fontWeight: FontWeight.w600,
      ),
    );

Widget _sectionDivider() => Padding(
      padding: EdgeInsets.symmetric(vertical: 12.h),
      child: Divider(
        thickness: 0.5,
        color: Colors.grey.withValues(alpha: 0.3),
      ),
    );