🍕 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,TextBodyyTextCaption. - 🔥 Paginación Firebase + Riverpod:
PaginationController,SliverListCustomySliverGridCustom. - 🍏 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 exportaremospizzacorn_uidesde 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
- No uses
consten widgets de Pizzacorn UI. Muchos leen tokens dinámicos en runtime. - Textos con string posicional:
TextBody("Hola"), noTextBody(text: "Hola"). - Espaciado con tokens: usa
Space(SPACE_SMALL),PADDING,PADDING_ALL, etc. - Paginación con placeholder:
SliverListCustomySliverGridCustomsiempre deben recibiritemPlaceholder. - Firestore con modelos: el
fromJsondePaginationParams<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
DropdownCustom<T>
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);
},
)
DropdownSearch<T>
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. 🍕✨