🍕 Pizzacorn UI

Pizzacorn UI es el design system oficial de Pizzacorn para Flutter: widgets reutilizables, tokens visuales, tipografía configurable, helpers de navegación, componentes responsive y un motor de paginación para Firestore listo para producción.

Está pensado para construir apps rápidas, limpias y con una identidad visual consistente en móvil, web y desktop.


✨ Qué incluye

  • 🎨 Theme tokens dinámicos: colores, paddings, radios, alturas y breakpoints configurables.
  • 🔤 Sistema tipográfico: textos semánticos como TextTitle, TextBody y TextCaption.
  • 🔥 Paginación Firebase + Riverpod: PaginationController, SliverListCustom y SliverGridCustom.
  • 🍏 Segmented controls estilo Apple: controles simples, Cupertino y tipo tabs.
  • 🧩 Widgets custom: botones, inputs, appbars, overlays, imágenes, dropdowns, calendarios y helpers.
  • 💻 Adaptadores responsive: layouts y popups adaptados a web/escritorio.

📦 Instalación

Añade la dependencia en tu pubspec.yaml:

dependencies:
  pizzacorn_ui:
    git:
      url: https://github.com/Pizzacorn/pizzacorn_ui

Importa la librería en tus archivos Dart:

import 'package:pizzacorn_ui/pizzacorn_ui.dart';

Regla Pizzacorn: en los proyectos de app usamos barrel import desde lib/config/imports.dart, así que normalmente exportaremos pizzacorn_ui desde ahí y cada archivo de pantalla importará solo ese archivo global.


🚀 Configuración inicial

Configura colores, layout y tipografías antes de runApp().

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  ConfigurePizzacornColors(
    accent: const Color(0xFF5256D6),
    background: const Color(0xFFFFFFFF),
    backgroundSecondary: const Color(0xFFF4F4F6),
    text: const Color(0xFF151727),
    subtext: const Color(0xFF8E929E),
  );

  ConfigurePizzacornLayout(
    radius: 8,
    paddingSize: 20,
    paddingSmallSize: 10,
    buttonHeight: 44,
    fieldHeight: 44,
    webSize: 1100,
  );

  PizzacornTextConfig.configure(
    primaryFontFamily: 'Montserrat',
    secondaryFontFamily: 'Inter',
    textSet: const PizzacornTextSet(
      big: PizzacornTextStyleSet(
        size: 32,
        fontWeight: FontWeight.w700,
      ),
      title: PizzacornTextStyleSet(
        size: 22,
        fontWeight: FontWeight.w700,
      ),
      subtitle: PizzacornTextStyleSet(
        size: 18,
        fontWeight: FontWeight.w600,
      ),
      body: PizzacornTextStyleSet(
        size: 14,
        fontWeight: FontWeight.w400,
      ),
      button: PizzacornTextStyleSet(
        size: 14,
        fontWeight: FontWeight.w700,
      ),
      caption: PizzacornTextStyleSet(
        size: 12,
        fontWeight: FontWeight.w400,
      ),
      small: PizzacornTextStyleSet(
        size: 10,
        fontWeight: FontWeight.w400,
      ),
    ),
  );

  ConfigurePizzacornPagination(
    databaseName: 'mi-base-firestore',
  );

  runApp(MyApp());
}

ConfigurePizzacornPagination es opcional. Si no lo llamas, o si pasas null, '' o '(default)', la paginación seguirá usando la base Firestore (default).


🧠 Reglas de oro

  1. No uses const en widgets de Pizzacorn UI. Muchos leen tokens dinámicos en runtime.
  2. Textos con string posicional: TextBody("Hola"), no TextBody(text: "Hola").
  3. Espaciado con tokens: usa Space(SPACE_SMALL), PADDING, PADDING_ALL, etc.
  4. Paginación con placeholder: SliverListCustom y SliverGridCustom siempre deben recibir itemPlaceholder.
  5. Firestore con modelos: el fromJson de PaginationParams<T> debe construir tu modelo.

🎨 Design tokens

Colores

Token Uso
COLOR_BACKGROUND Fondo principal
COLOR_BACKGROUND_SECONDARY Fondos secundarios, inputs o scaffolds
COLOR_BACKGROUND_TERCIARY Superficies elevadas
COLOR_ACCENT Acción principal y marca
COLOR_ACCENT_SECONDARY Acción secundaria
COLOR_TEXT Texto principal
COLOR_SUBTEXT Texto secundario
COLOR_TEXT_BUTTONS Texto sobre botones de acento
COLOR_BORDER Bordes y contornos
COLOR_DIVIDER Separadores
COLOR_ERROR Errores
COLOR_ALERT Avisos
COLOR_DONE Éxitos
COLOR_INFO Información

Layout

Token Uso
RADIUS Radio global
WEBSIZE Breakpoint responsive
BUTTON_HEIGHT Altura estándar de botones
FIELD_HEIGHT Altura estándar de campos
DOUBLE_PADDING Padding base numérico
DOUBLE_PADDING_SMALL Padding compacto numérico
PADDING Padding horizontal base
PADDING_SMALL Padding horizontal compacto
PADDING_ALL Padding completo base
PADDING_ALL_SMALL Padding completo compacto

Espacios

Token Valor Uso
SPACE_BIGGER 60 Separación grande entre bloques
SPACE_BIG 40 Separación amplia
SPACE_MEDIUM 20 Separación estándar
SPACE_SMALL 10 Elementos relacionados
SPACE_SMALLEST 5 Ajustes finos
Column(
  children: [
    TextTitle("Resumen"),
    Space(SPACE_SMALL),
    TextBody("Contenido de la sección"),
  ],
)

🔤 Textos

Los widgets de texto usan la configuración global de PizzacornTextConfig y los colores del tema.

Widget Uso recomendado
TextBig("Texto") Mensajes de gran impacto
TextTitle("Texto") Títulos de sección
TextSubtitle("Texto") Subtítulos
TextBody("Texto") Texto estándar
TextButtonCustom("Texto") Texto dentro de botones
TextCaption("Texto") Texto secundario, fechas, ayudas
TextSmall("Texto") Microtexto
TextCustom("Texto") Control manual de tamaño, peso y espaciado
Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    TextBig("Dashboard"),
    Space(SPACE_SMALL),
    TextTitle("Ventas de hoy"),
    TextBody("Aquí se muestra el rendimiento de la tienda."),
    TextCaption("Actualizado hace 2 minutos"),
  ],
)

También puedes usar estilos directamente:

Text(
  "Texto nativo con estilo Pizzacorn",
  style: styleBody(color: COLOR_TEXT, fontWeight: WEIGHT_BOLD),
)

🔥 Paginación Firestore

La paginación combina:

  • PaginationParams<T>: configuración de colección, query y parser.
  • paginationProvider: provider Riverpod auto-dispose.
  • PaginationController<T>: carga inicial, refresh y fetch more.
  • SliverListCustom<T> / SliverGridCustom<T>: render con skeleton, vacío, error y scroll infinito.

0. Configuración opcional de base de datos

Por defecto se usa la base Firestore (default). Si tu proyecto usa otra base de datos, configúrala una vez al iniciar la app:

ConfigurePizzacornPagination(
  databaseName: 'mi-base-firestore',
);

Para volver a (default), no llames a la función o pásale null, '' o '(default)'.

1. Define los parámetros

final params = PaginationParams<UserModel>(
  collection: "users",
  limit: 20,
  identifier: "users_home",
  fromJson: (data) => UserModel.fromJson(data),
  query: (q) => q.orderBy("createdAt", descending: true),
);

2. Lista paginada

class UsersPage extends ConsumerWidget {
  UsersPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final params = PaginationParams<UserModel>(
      collection: "users",
      limit: 20,
      identifier: "users_home",
      fromJson: (data) => UserModel.fromJson(data),
      query: (q) => q.orderBy("createdAt", descending: true),
    );

    return Scaffold(
      backgroundColor: COLOR_BACKGROUND,
      body: CustomScrollView(
        slivers: [
          CupertinoSliverRefreshControl(
            onRefresh: () {
              return ref.read(paginationProvider(params).notifier).refresh();
            },
          ),
          SliverPadding(
            padding: PADDING,
            sliver: SliverListCustom<UserModel>(
              params: params,
              itemPlaceholder: UserModel(),
              idExtractor: (userModel) => userModel.id,
              itemBuilder: (userModel) {
                return UserWidget(userModel: userModel);
              },
            ),
          ),
        ],
      ),
    );
  }
}

3. Grid paginado

SliverGridCustom<ProductModel>(
  params: productParams,
  itemPlaceholder: ProductModel(),
  crossAxisCount: 2,
  childAspectRatio: 0.78,
  mainAxisSpacing: SPACE_SMALL,
  crossAxisSpacing: SPACE_SMALL,
  idExtractor: (productModel) => productModel.id,
  itemBuilder: (productModel) {
    return ProductCard(productModel: productModel);
  },
)

Estados incluidos

Estado Qué hace
isLoading Muestra skeleton inicial
isFetchingMore Muestra skeleton inferior
hasMore Controla si se solicita la siguiente página
error Muestra error inicial o error al cargar más

🍏 Segmented controls

SegmentedCupertinoCustom

Ideal para filtros principales con look nativo Apple.

SegmentedCupertinoCustom(
  items: ["Hoy", "Semana", "Mes"],
  itemsSecondary: ["12", "48", "130"],
  currentIndex: state.currentTab,
  onValueChanged: (index) {
    controller.changeTab(index);
  },
)

SegmentedControlCustom

Control custom con thumb animado y altura configurable.

SegmentedControlCustom(
  items: ["Pendientes", "Pagadas"],
  currentIndex: state.currentStatus,
  height: 38,
  thumbRadius: RADIUS,
  onValueChanged: (index) {
    controller.changeStatus(index);
  },
)

SegmentedTabCustom

Selector tipo tab con línea inferior animada.

SegmentedTabCustom(
  items: ["Explorar", "Mis reservas"],
  currentIndex: state.currentTab,
  onValueChanged: (index) {
    controller.changeTab(index);
  },
)

🔘 Botones

ButtonCustom

Botón principal con soporte para texto, rich text, iconos IconData, SVG, borde, gradiente y semántica.

ButtonCustom(
  text: "Guardar cambios",
  prefixIcon: Icons.save_rounded,
  onPressed: () {
    controller.save();
  },
)
ButtonCustom(
  text: "Cancelar",
  border: true,
  color: Colors.transparent,
  textColor: COLOR_TEXT,
  borderColor: COLOR_BORDER,
  onPressed: () => Navigator.pop(context),
)

ButtonCustomIcon

Botón solo icono con fondo, borde, SVG o IconData.

ButtonCustomIcon(
  icon: Icons.edit_rounded,
  color: COLOR_TEXT,
  colorBackground: COLOR_BACKGROUND_SECONDARY,
  borderColor: COLOR_BORDER,
  borderWidth: 1,
  onPressed: () {
    controller.edit();
  },
)

OptionButton

Fila accionable para ajustes, perfiles o menús.

OptionButton(
  title: "Métodos de pago",
  prefixIcon: Icons.credit_card_rounded,
  suffixIcon: Icons.chevron_right_rounded,
  onTap: () => goTo(context, PaymentsPage()),
)

MoreMenuButton

Menú rápido de acciones comunes: editar, eliminar, duplicar y reportar.

MoreMenuButton(
  onEdit: () {
    controller.edit();
  },
  onDelete: () {
    controller.delete();
  },
)

📝 Formularios e inputs

TextFieldCustom

Campo de texto con tokens visuales, iconos, password, validación, max length, teclado numérico y shadow.

TextFieldCustom(
  controller: nameController,
  hintText: "Nombre",
  prefixIcon: Icons.person_rounded,
  errorText: "Escribe un nombre",
  onChanged: (value) {
    controller.changeName(value);
  },
)
TextFieldCustom(
  controller: passwordController,
  hintText: "Contraseña",
  password: true,
)

Otros campos

Widget Uso
TextFieldPhoneCustom Teléfono con selector de país
DatePickerField Campo que abre selector de fecha
TimePickerField Campo que abre selector de hora
TitleAndTextField Título + campo en bloque
SelectorList Lista de opciones seleccionables
CheckboxPolitics Checkbox para políticas/condiciones

🔽 Dropdowns

Dropdown basado en PopupMenuButton, con nombre por función.

DropdownCustom<CategoryModel>(
  items: categories,
  initialItem: state.categoryModel,
  hintText: "Selecciona categoría",
  tooltip: "Categorías",
  getName: (categoryModel) => categoryModel.name,
  onChanged: (categoryModel) {
    controller.changeCategory(categoryModel);
  },
)

Versión con búsqueda para listas más largas.

DropdownSearch<UserModel>(
  items: users,
  initialItem: state.userModel,
  hintText: "Buscar usuario",
  tooltip: "Usuarios",
  getName: (userModel) => userModel.name,
  onChanged: (userModel) {
    controller.changeUser(userModel);
  },
)

🧭 AppBars y navegación

Helper / Widget Uso
AppBarBack AppBar con back estándar
AppBarBackAction AppBar con back y acción
AppBarClose AppBar con cierre
AppBarDrag Barra para bottom sheets o zonas drag
AppBarHome AppBar principal con menú, logo y perfil
goTo Navega con back
goToNoBack Navega sin animación/back estándar
goToClear Limpia stack y navega
goBack Cierra pantalla devolviendo resultado opcional
appBar: AppBarBack(
  context: context,
  title: "Detalle",
)
goTo(context, UserDetailPage(userModel: userModel));

🧊 Overlays, loading y sheets

Loading

Envuelve una pantalla y muestra bloqueo cuando loading es true.

Loading(
  loading: state.isLoading,
  child: CustomScrollView(
    slivers: [
      SliverToBoxAdapter(
        child: TextTitle("Contenido"),
      ),
    ],
  ),
)

LoadingWithText

LoadingWithText(
  loading: state.isLoading,
  text: "Guardando...",
  child: body,
)

LoadingCustomWidget

Loader compacto para botones, cards o zonas pequeñas donde no necesitas bloquear toda la pantalla.

LoadingCustomWidget(
  size: 22,
  strokeWidth: 2.5,
)

Bottom sheets

openBottomSheet(
  context,
  BottomSheetCustomTwoButtons(
    leftTitle: "Cancelar",
    rightTitle: "Eliminar",
    onLeftPressed: () => goBack(context),
    onRightPressed: () => controller.delete(),
    rightColor: COLOR_ERROR,
  ),
);

BottomSheetPopUps

Barra inferior de acciones para popups o editores con acción principal y eliminación opcional.

BottomSheetPopUps(
  title: "Guardar",
  onPressedSave: () {
    controller.save();
  },
  onPressedDelete: () {
    controller.delete();
  },
)

Widgets disponibles:

Widget / Helper Uso
Loading Overlay bloqueante con loader centrado
LoadingWithText Overlay bloqueante con texto de estado
LoadingCustomWidget Loader compacto reutilizable
BottomSheetCustomOneButton Sheet inferior con un botón
BottomSheetCustomTwoButtons Sheet inferior con dos botones
BottomSheetPopUps Barra inferior para guardar/eliminar en popups
openBottomSheet Abre bottom sheet estándar con altura configurable
openStupidSheet Abre sheet flotante usando stupid_simple_sheet
openStupidCupertinoSheet Abre sheet Cupertino con navegación integrada
openBottomNoBack Sheet no descartable
openDialog Dialog helper
openSnackbar Snackbar helper con estados isError, isAlert, isDone

🖼️ Imágenes, iconos y media

ImageCustom

Imagen de red con placeholder, borde, radio, modo circular y overlay.

ImageCustom(
  imageUrl: productModel.image,
  height: 180,
  borderRadius: RADIUS,
  overlay: Align(
    alignment: Alignment.bottomLeft,
    child: Padding(
      padding: PADDING_ALL_SMALL,
      child: TextCaption("Nuevo", color: COLOR_TEXT_BUTTONS),
    ),
  ),
)

Otros widgets

Widget / Helper Uso
ProfileImageCustom Avatar de usuario
FullScreenImagePage Visor de imagen a pantalla completa
SvgCustom SVG con color
SvgCustomNoColor SVG sin recolorear
FeaturedIconCustom Icono destacado
FeaturedTitle Bloque con icono y título
FeaturedSubtitle Bloque con icono, título y subtítulo
IconPickerCustom Selector de iconos Material
IconGalleryPage Galería de iconos UIcons

📅 Calendarios y pickers

Widget Uso
MonthlyCalendar Calendario mensual con selección simple o por rango
SliderCalendar Calendario horizontal
DatePickerCustom Selector custom de fecha
TimePickerCustom Selector custom de hora
IconPickerCustom Selector de IconData en grid
IconGalleryPage Galería buscable de iconos UIcons Pro
DatePickerField Campo de formulario que abre selector de fecha
TimePickerField Campo de formulario que abre selector de hora
MonthlyCalendar(
  startDate: DateTime.now(),
  initialDate: state.selectedDate,
  selectionMode: CalendarSelectionMode.single,
  markedDates: state.reservationDates,
  blockedWeekdays: [DateTime.sunday],
  style: CalendarStyle(),
  onDaySelected: (date) {
    controller.changeDate(date);
  },
)

DatePickerCustom

Picker Cupertino para fecha, pensado para abrirse dentro de un bottom sheet. Soporta fecha mínima y máxima.

openBottomSheet(
  context,
  DatePickerCustom(
    initialDateTime: state.selectedDate,
    minimumDate: DateTime.now(),
    maximumDate: DateTime.now().add(Duration(days: 365)),
    onDateTimeChanged: (date) {
      controller.changeDate(date);
    },
  ),
  height: 350,
);

TimePickerCustom

Picker Cupertino para hora en formato 24h.

openBottomSheet(
  context,
  TimePickerCustom(
    initialDateTime: state.selectedTime,
    onDateTimeChanged: (date) {
      controller.changeTime(date);
    },
  ),
  height: 350,
);

IconPickerCustom

Selector visual de IconData. Puedes usar la lista interna por defecto o pasar una lista propia.

IconPickerCustom(
  selectedIcon: state.icon,
  accentColor: COLOR_ACCENT,
  crossAxisCount: 8,
  onSelected: (icon) {
    controller.changeIcon(icon);
  },
)

IconGalleryPage

Galería de iconos UIcons Pro con búsqueda, paginación interna y selección por nombre.

goTo(
  context,
  IconGalleryPage(
    initialSelectedIconName: state.iconName,
    onIconSelected: (iconName) {
      controller.changeIconName(iconName);
    },
  ),
);

💻 Responsive web

Widget Uso
WebAdapterCustom Adapta layouts según WEBSIZE
WebPopupCustom Contenedor/popup optimizado para desktop
HoverCustom Feedback visual en hover
BlurCustom Efecto blur
ShowUpCustom Animación de entrada
ShimmerCustom Placeholder shimmer

🌍 Multiidioma

Pizzacorn UI incluye helpers para cargar traducciones desde Excel y selectores de idioma.

await initMultilanguage(defaultLang: 'es');
Widget / Helper Uso
loadExcelTranslations Carga traducciones desde Excel
initMultilanguage Inicializa localización
LanguageSelector Selector completo de idioma
LanguageSmallSelector Selector compacto

Los colores del panel se configuran por separado de los del selector compacto:

LanguageSmallSelector(
  sheetBackgroundColor: Color(0xFF181818),
  sheetItemBackgroundColor: Color(0xFF242424),
  sheetSelectedBackgroundColor: Color(0xFF343454),
  sheetTextColor: Colors.white,
  sheetSelectedTextColor: Color(0xFFB9BAFF),
  sheetSelectedBorderColor: Color(0xFF7779DD),
  sheetDragHandleColor: Colors.white38,
)

En LanguageSelector directamente, los parámetros equivalentes son backgroundColor, itemBackgroundColor, selectedBackgroundColor, textColor, selectedTextColor, selectedBorderColor y dragHandleColor. Si se omiten, se mantienen los colores actuales del tema. El indicador de selección usa también selectedTextColor. | getFlagEmoji | Devuelve bandera por código |


🧰 Catálogo rápido

Categoría Widgets / helpers
Textos TextBig, TextTitle, TextSubtitle, TextBody, TextButtonCustom, TextCaption, TextSmall, TextCustom
Paginación ConfigurePizzacornPagination, PizzacornPaginationConfig, PaginationParams, PaginationState, paginationProvider, SliverListCustom, SliverGridCustom
Segments SegmentedCupertinoCustom, SegmentedControlCustom, SegmentedTabCustom
Botones ButtonCustom, ButtonCustomIcon, OptionButton, MoreMenuButton, PopupMenuOptions
Formularios TextFieldCustom, TextFieldPhoneCustom, DatePickerField, TimePickerField, TitleAndTextField, SelectorList, CheckboxPolitics
Dropdowns DropdownCustom, DropdownSearch
AppBars AppBarBack, AppBarBackAction, AppBarClose, AppBarDrag, AppBarHome
Overlays Loading, LoadingWithText, LoadingCustomWidget, BottomSheetCustomOneButton, BottomSheetCustomTwoButtons, BottomSheetPopUps, openBottomSheet, openStupidSheet, openStupidCupertinoSheet, openBottomNoBack, openDialog, openSnackbar
Imágenes ImageCustom, ProfileImageCustom, FullScreenImagePage, ImagePublish, CropPage
Iconos SvgCustom, SvgCustomNoColor, FeaturedIconCustom, FeaturedTitle, FeaturedSubtitle, IconPickerCustom, IconGalleryPage
Calendarios y pickers MonthlyCalendar, SliderCalendar, DatePickerCustom, TimePickerCustom, IconPickerCustom, IconGalleryPage, DatePickerField, TimePickerField
Navegación goTo, goToNoBack, goToClear, goBack, openBottomSheet, openDialog, openSnackbar
Decoración DecorationCustom, BoxDecorationCustom, BorderRadiusCustomAll, BoxShadowCustom
Efectos ShimmerCustom, ShowUpCustom, HoverCustom, BlurCustom
Login LoginCustomPage, SelectorCustomPage, LoginCustomAuthRepository, VerifyEmailCustomPage
Modelos FileModel, SocialModel
Utils parseDate, BestOnColor, ContrastRatio, closeKeyboard, UIconsMappingHelper

✅ Ejemplo de pantalla Pizzacorn

class ReservationsPage extends ConsumerWidget {
  ReservationsPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(reservationsProvider);
    final controller = ref.read(reservationsProvider.notifier);
    final params = controller.getPaginationParams();

    return Scaffold(
      backgroundColor: COLOR_BACKGROUND,
      body: Loading(
        loading: state.isLoading,
        child: CustomScrollView(
          slivers: [
            ReservationsAppBar(),
            CupertinoSliverRefreshControl(
              onRefresh: () {
                return ref.read(paginationProvider(params).notifier).refresh();
              },
            ),
            SliverPadding(
              padding: PADDING,
              sliver: SliverToBoxAdapter(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    TextTitle("Reservas"),
                    Space(SPACE_SMALL),
                    TextBody("Gestiona las próximas reservas."),
                    Space(SPACE_MEDIUM),
                    SegmentedCupertinoCustom(
                      items: ["Próximas", "Historial"],
                      currentIndex: state.currentTab,
                      onValueChanged: (index) {
                        controller.changeTab(index);
                      },
                    ),
                    Space(SPACE_MEDIUM),
                  ],
                ),
              ),
            ),
            SliverPadding(
              padding: PADDING,
              sliver: SliverListCustom<ReservationModel>(
                params: params,
                itemPlaceholder: ReservationModel(),
                idExtractor: (reservationModel) => reservationModel.id,
                itemBuilder: (reservationModel) {
                  return ReservationWidget(
                    reservationModel: reservationModel,
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

🧑‍🍳 Equipo

Hecho con mimo por el equipo Pizzacorn.

Porque una app sin sistema visual es como una pizza sin queso: puede existir, pero no debería. 🍕✨

Libraries

pizzacorn_ui