Guiadalajara POS — Plan de paridad total Web ↔ Flutter

14 de septiembre de 2026 · plan de arquitectura · design system + vistas + backend, con tokens compartidos como fuente única

Decisiones de arranque (Gustavo, 14-sep): alcance = todo (DS + 66 vistas + backend/GraphQL, incluidos los 6 scaffolds) · mecanismo = tokens compartidos como fuente única (un origen → CSS + Dart; componentes a mano en cada lado bebiendo del mismo token; matriz de paridad como control).


1. Resumen ejecutivo

El POS de Guiadalajara vive hoy en dos superficies Web (terminal @guiadalajara/pos + consola admin-shell) y va hacia tres apps Flutter (Caja, Cocina, Gestión) + displays LAN. Queremos que el POS entero exista en ambos mundos y no se desincronice. Este plan lo estructura en tres capas de paridad, todas ancladas a una fuente única de tokens:

El control anti-deriva no es disciplina sola: es tokens generados desde un origen + matriz de paridad viva + pruebas (visual regression del DS, contract tests del GraphQL) en CI.


2. Objetivo y alcance

Objetivo: que un cambio de marca o de contrato se propague a Web y Flutter desde un solo lugar, y que "estamos en paridad" sea medible, no una aspiración.

Dentro de alcance (las 3 capas): - DS: ~35 componentes ya medidos en Flutter (gdl_ui) + su contraparte Web + los Pos* nuevos. - Vistas: las 66 del TRACKER (Caja 23, Cocina 4, Gestión 37, Display 2) × Web/Flutter. - Backend: 38 servicios (32 REAL, 6 SCAFFOLD) + el contrato GraphQL que ambos frontends comparten.

Fuera de alcance (por ahora, se nombran para acotar): terminal card-present, agregadores de delivery, y todo lo que no sea POS (marketplace consumer, etc.).

No-objetivos: no unificar los frameworks (Web sigue React, móvil sigue Flutter); no codegen de componentes (se descartó); la paridad es de contrato + apariencia + comportamiento, no de código.


3. Estado actual medido (baseline de paridad)

Todo ✅ = medido en el repo nuevo apps/marketplace/flutter y ~/guiadalajara-project (monorepo). El baseline Web se completa con las mediciones en curso.

3.1 Flutter (✅ medido)

3.2 Web (✅ medido)

3.3 Backend (✅ medido)

3.4 Diagnóstico de deriva (medido)

La buena noticia: los tres frontends ya son el mismo DS SCMDG Soft DS-3, con valores idénticos (verificado por diff y por el comentario "ported 1:1" en el Dart). La mala: son tres copias mantenidas a mano sin fuente única: - apps/marketplace/pos/app/src/styles/tokens.css (CSS) - apps/admin-shell/src/styles/tokens.css (CSS, copia 1:1 del anterior) - apps/marketplace/flutter/packages/gdl_theme/lib/src/scmdg/ds_colors.dart (Dart, "Ported 1:1 from tokens/colors.css")

No existe paquete de tokens compartido (verificado: sin packages/tokens, sin style-dictionary, sin tokens.json). La "fuente" conceptual de la que todos dicen portar es tokens/colors.css del skill guiadalajara-design. Hoy coinciden por disciplina; sin origen común, la próxima corrección de marca se aplicará en un lugar y divergirán. Señales de deuda ya presentes: bloque legacy --hig-green-* remapeado, index.html del admin aún con data-brand="nintaigo", el --num-tabular que ya solo está en admin. Eso es exactamente lo que la Capa 0 elimina.


4. Arquitectura — fuente única de tokens (Capa 0)

Origen: un paquete packages/design-tokens/ con los tokens en JSON declarativo (formato estilo W3C Design Tokens / Style Dictionary). Es la única verdad de color, tipografía, radio, espaciado, motion, sombra, con sus valores claro/oscuro.

Generación (build step, en CI): - → CSS (tokens.css con :root{--...} + [data-theme=dark]) que consumen el terminal POS, el admin-shell y el storybook Web. - → Dart (ds_tokens.g.dart que alimenta DsTokens/buildScmdgSoftTheme) para gdl_theme. - (Opcional) → Tailwind theme extend, si el Web usa Tailwind.

Regla: nadie escribe un hex/tamaño a mano en componentes; todo lee un token. El PR que agrega un token corre el generador y ambos lados se actualizan en un commit.

Reconciliación inicial (bajo riesgo — ya son copias idénticas): como los tres frontends ya portan 1:1 del mismo sistema, la extracción es mecánica, no de diseño. Semilla del JSON = tokens/colors.css del skill guiadalajara-design (la fuente conceptual que todos citan), cotejada contra gdl_theme/src/scmdg (la más completa). Luego: - el JSON genera el CSS que reemplaza las dos copias apps/.../pos/app/src/styles/tokens.css y apps/admin-shell/src/styles/tokens.css (hoy diff = 1 token); - el JSON genera el Dart que alimenta gdl_theme (reemplaza el hand-port de ds_colors.dart); - el CSS del directorio/storybook deja de hardcodear y consume el generado. Los Gdl* legacy y el bloque --hig-green-* quedan deprecados. La "línea de acento" se formaliza como utilidad/token (accent + border-width, esquina recta).


5. Capa 1 — Paridad de design system

Punto de partida real: el Web NO parte de cero — ya tiene dos paquetes de DS (@dockethub/ui primitivas, @dockethub/ui-restaurant dominio POS). El Flutter tiene las 31 Ds* primitivas pero le falta el dominio POS (pos_ui por crear). Es decir, en la capa de dominio Web va adelante. La paridad es alinear dos inventarios existentes y cerrar el hueco de Flutter.

Matriz de componentes (una fila por componente; columnas: Web / Flutter / estado / variantes). Mapeo natural en dos bloques:

Primitivas — @dockethub/ui ↔ gdl_ui/src/ds/: | Web (@dockethub/ui) | Flutter (Ds*) | |---|---| | Button, Badge, Card, Dialog, Input, Select, Table, Tabs, Stat, Toast, EmptyState, ErrorBanner, ActionMenu, Field, Header/PageHeader/Section, BarcodeInput, charts | DsButton, DsBadge, DsCard, DsDialog, DsInput, DsSelect, DsTable, DsTabs, DsStat, DsToast, (Gdl)EmptyState, (Gdl)ErrorView, DsMenu, DsInput(label), — , — , DsDashboardGrid+DsBarChart (por crear) |

Huecos a cerrar: Flutter tiene extras sin par Web (DsAccordion, DsBreadcrumbs, DsPagination, DsProgress, DsSlider, DsCheckbox, DsRadio, DsSwitch, DsTextarea, DsTooltip, DsAvatar, DsQrCode, DsSkeleton, DsSpinner) → verificar si Web los tiene sueltos o faltan; y Web tiene charts/BarcodeInput/Header sin par Flutter.

Dominio POS — @dockethub/ui-restaurant ↔ pos_ui (por crear en Flutter): | Web (@dockethub/ui-restaurant) | Flutter (Pos* propuesto) | |---|---| | NumPad | PosKeypad | | InteractiveFloor / FloorEditor | PosTableTile (+ editor) | | KdsColumn / TicketCard | PosKdsTicket | | OrderPanel | PosOrderCard | | TemplatePreview | PosReceipt | | CashDenominationsForm | (corte de caja) | | SplitDialog | (pago mixto — liga a pos-payments-mixed) | | RefundDialog (en el POS app) | (reembolsos — liga a pos-refunds) | | ChangeDueDisplay, TipSelector, CoversStepper, PrinterStatusBadge, LockScreen | PosDisplaySurface, tips, covers, etc. |

Insight: los Pos* que el catálogo proponía "crear" ya existen en Web como @dockethub/ui-restaurant. El trabajo Flutter es portarlos a pos_ui; Web solo necesita dedupe (ver §8).

Doble storybook (el "todo como Web y todo como Flutter"): - Web: Storybook real de @dockethub/ui + @dockethub/ui-restaurant (componentes vivos, no las réplicas CSS del directorio actual). El catálogo pos-directorio queda como vista de referencia/documentación. - Flutter: Widgetbook sobre gdl_ui/pos_ui, desplegado como Flutter web junto al de Web. - Ambos leen los tokens generados (Capa 0) → misma apariencia por construcción.

Control: visual-regression por componente (mismo set de estados y ambos temas) para detectar divergencia Web↔Flutter.


6. Capa 2 — Paridad de vistas

Mapa de vistas (las 66 del TRACKER) con su equivalente en cada mundo: - Caja (cj-01..23) ↔ terminal @guiadalajara/pos. - Cocina (kd-01..04) ↔ vista KDS del terminal / app Cocina. - Gestión (ge-01..37) ↔ admin-shell. - Display LAN (dp-01/02) ↔ superficies LAN emitidas por la app Caja (no hay equivalente Web público — es panel LAN).

Cada vista: misma información, misma navegación (breadcrumbs ya documentados), mismos estados (vacío/carga/error). Se construyen sobre los componentes de la Capa 1, así que la paridad de vista se apoya en la de componente.

Orden sugerido: por dominio, empezando por los flujos de dinero (cobro, corte, tickets, recibo) y el piso/comanda, luego Gestión por módulos (inventario → recetario → catálogo → …).


7. Capa 3 — Paridad de backend / datos

Principio: el contrato GraphQL es la fuente; ambos frontends generan sus tipos desde el mismo schema (codegen de tipos, no de UI). Paridad de datos = ambos consumen las mismas queries/mutations.

Cerrar los 6 scaffolds (los huecos reales): - 🔴 P1 pos-refunds — reembolsos (cableado en Terminal, servicio hueco). - 🔴 P1 pos-payments-mixed — pago mixto/split por método. - P2 kds-stations + kds-priorities — configuración del tablero KDS. - P2 pos-portal-facturacion — portal de autofactura del cliente. - P3 pos-sessions — redundante; consolidar en pos-floor, no reimplementar.

Control: contract tests contra el schema; el codegen de tipos en CI rompe el build si un frontend usa un campo que el schema no expone.


8. Mecanismo anti-deriva (cómo NO se desincroniza)

  1. Tokens generados (Capa 0) — imposible que un color difiera si ambos leen el mismo generado.
  2. Matriz de paridad viva — un archivo/tablero que lista cada componente y vista con su estado Web/Flutter; parte del repo, se revisa en cada PR.
  3. Visual regression del DS — snapshots por componente y tema; falla si Web y Flutter divergen visualmente.
  4. Contract tests + type codegen del GraphQL — el schema es la verdad; el build rompe ante deriva de datos.
  5. Definición de "hecho": una pieza está en paridad cuando existe en ambos, lee tokens, tiene sus variantes, pasa visual-regression y (si aplica) contract test.

9. Roadmap por fases

(Fases secuenciadas por dependencia, no por calendario — F0 es prerrequisito; F1-F3 pueden solaparse por dominio.)


10. Riesgos y mitigaciones


11. Métricas de paridad y próximos pasos

Métricas (cómo se mide la paridad): - % de componentes en la matriz presentes en ambos con visual-regression verde. - % de las 66 vistas presentes en ambos. - Nº de tokens hardcodeados fuera del origen (meta: 0). - Nº de scaffolds cerrados (meta: 6/6, o 5 + pos-sessions consolidado). - Nº de campos GraphQL usados por un frontend sin respaldo de schema (meta: 0).

Próximos pasos (para arrancar F0): 1. Crear packages/design-tokens con el JSON semilla desde guiadalajara-design/tokens/colors.css (cotejado con gdl_theme/src/scmdg). 2. Escribir los dos generadores (CSS, Dart) y cablearlos en CI. 3. Reemplazar la primera de las dos copias CSS (apps/marketplace/pos/app/src/styles/tokens.css) por el generado — es la prueba de que un origen alimenta a los tres consumidores. 4. Levantar la matriz de paridad viva en el repo (con el mapeo de §5 como semilla). 5. En paralelo: deduplicar los wrappers app-level repetidos entre POS y admin-shell (ThemeSwitcher, ConfirmDialog, BrandMark, DataTable, AvatarMenu, Toast, Tabs, Skeleton) subiéndolos a @dockethub/ui.


Servicios Creativos — Guadalajara, México

guiadalajara.comServicios Creativos — Guadalajara, MX