Tareas #8569

Actualizado por Anielkis Herrera hace 17 días

h1. Tarea: TimelineWidget — Contenedor y Ensamblaje Feature: Widget Timeline de Partido (General)

h2. Contexto

Esta tarea cubre la creación del *widget completo de Línea de Tiempo*:
*Objetivo:* Implementar el contenedor orquestador, la barra horizontal, la lista de eventos por equipo, la pista central y el reloj del partido.

Reference HTML:
componente orquestador %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}docs/examples/timeline.html%
*Prerequisito:* %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}timeline-items.md% (todos los %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}*TimeCard% deben existir antes
3px;font-weight:bold;}MatchTimeline.vue% que gestiona la visualización cronológica de ensamblar el widget). un partido, integrando la lógica de negocio mediante composables y delegando la UI en componentes específicos.

---

h2. 1. Componentes a Crear Ubicación y Nomenclatura

|_. Componente |_. Ruta |_. Categoría |
|@TimelineBar.vue@|@components/sports/football/general/@|Negocio General|
|@TimelineEventList.vue@|@components/sports/football/specific/@|Negocio Específico|
|@TimelineTrack.vue@|@components/sports/football/specific/@|Negocio Específico|
|@MatchClock.vue@|@components/sports/football/specific/@|Negocio Específico|
|@TimelineWidget.vue@|@src/widgets/timeline/@|Widget (entry point)|
De acuerdo al criterio de *Componentes de Negocio (General)*, este componente debe actuar como controlador:

---

h2. 2.
* *Archivo:* %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}TimelineEventList.vue% — Lista de Eventos por Equipo

*Responsabilidad:* Renderiza la lista de eventos de *un equipo* (home o away), calcula la posición
3px;font-weight:bold;}MatchTimeline.vue%
* *Ruta:*
%{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}left% 3px;font-weight:bold;}src/components/sports/football/general/%
* *Responsabilidad:* Importar el store, servicios
y el apilamiento vertical composable de cada evento, y selecciona el componente lógica para distribuir los datos a los componentes %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}*TimeCard% correcto según el tipo. 3px;font-weight:bold;}specific% .

*Referencia (Bootstrap 5):* h2. 2. Implementación Técnica

<pre><code class="html">
<ul class="list-unstyled d-flex position-relative m-0 p-0 side-home">
<!-- Tarjeta amarilla min 23 -->
<li class="position-absolute" style="left: 149px; bottom: 0px;">
<div class="badge bg-warning text-dark px-1 py-1">Y</div>
</li>
<!-- Gol min 44 -->
<li class="position-absolute" style="left: 290px; bottom: 0px;">
<img src="goal.svg" class="w-20" />
</li>
<!-- Sustitución min 69 (apilada) -->
<li class="position-absolute" style="left: 492px; bottom: 0px;">
<i class="bi bi-arrow-left-right text-success"></i>
</li>
<li class="position-absolute" style="left: 492px; bottom: 17px;">
<i class="bi bi-arrow-left-right text-success"></i>
</li>
</ul>
</code></pre>

h3. Props A. Lógica de Negocio (Composable)

|_. Prop |_. Tipo |_. Requerido |_. Default |_. Descripción |
|@events@|@Array<MatchEvent>@|Sí|—|Lista
Toda la manipulación de eventos del equipo|
|@side@|@String@|Sí|—|@'home'@ \|@'away'@|
|@matchDuration@|@Number@|No|@90@|Minutos totales del partido (para escalar posiciones)|
|@totalWidthPx@|@Number@|No|@700@|Ancho disponible de la barra
datos debe vivir en px|

h3. Lógica de posicionamiento

<pre><code class="js">
// positionPx — posición horizontal proporcional al minuto
const positionPx = (event) => {
return (event.minutes / props.matchDuration) * props.totalWidthPx
}

// stackOffset — apilamiento vertical
el composable para eventos simultáneos
// Agrupa eventos por minuto y asigna offset incremental de 17px
const enrichedEvents = computed(() => {
const minuteCount = {}
return props.events.map((e) => {
const key = `${e.minutes}`
minuteCount[key] = minuteCount[key] ?? 0
const offset = minuteCount[key] * 17
minuteCount[key]++
return { ...e, positionPx: positionPx(e), stackOffset: offset }
})
})
</code></pre>

h3. Mapeo de tipo →
mantener el componente limpio (máximo ~200 líneas):

|_. @event.type@ |_. TypeId (JSON) |_. Componente |
|@'goal'@|9, 16|@GoalTimeCard@|
|@'yellow-card'@|3|@CardTimeCard@ con @cardType="yellow"@|
|@'red-card'@|5|@CardTimeCard@ con @cardType="red"@|
|@'yellow-red'@|-|@CardTimeCard@ con @cardType="yellow-red"@|
|@'substitution'@|7|@SubstitutionTimeCard@|
|@'var'@|568|@VarTimeCard@|

h3. Implementación completa

<pre><code class="vue">
<!-- src/components/sports/football/specific/TimelineEventList.vue -->
<template>
<ul :class="['list-unstyled m-0 p-0 position-absolute w-100', `side-${side}`]">
<component
v-for="event in enrichedEvents"
:key="event.incidenceId"
:is="componentFor(event.typeId)"
:event="event"
:positionPx="event.positionPx"
:stackOffset="event.stackOffset"
:homeTeamId="homeTeamId"
@item-click="$emit('event-click', $event)"
/>
</ul>
</template>

<script setup>
import { computed } from 'vue'
import GoalTimeCard from './GoalTimeCard.vue'
import CardTimeCard from './CardTimeCard.vue'
import SubstitutionTimeCard from './SubstitutionTimeCard.vue'
import VarTimeCard from './VarTimeCard.vue'

const props = defineProps({
events: { type: Array, required: true },
side: { type: String, required: true },
homeTeamId: { type: Number, required: true },
matchDuration: { type: Number, default: 90 },
totalWidthPx: { type: Number, default: 700 },
})
defineEmits(['event-click'])

// El TYPE_MAP idealmente mapea los typeId del JSON de producción
const TYPE_MAP = {
9: GoalTimeCard, // Goal by play
16: GoalTimeCard, // Goal
3: CardTimeCard, // Yellow card
5: CardTimeCard, // Red card
7: SubstitutionTimeCard, // Substitution
568: VarTimeCard, // Start VAR
}
const componentFor = (typeId) => TYPE_MAP[typeId] ?? null

const enrichedEvents = computed(() => {
const minuteCount = {}
return props.events.map((e) => {
const key = `${e.minutes}`
minuteCount[key] = minuteCount[key] ?? 0
const stackOffset = minuteCount[key]
* 17
minuteCount[key]++
const positionPx = (e.minutes / props.matchDuration) * props.totalWidthPx
return { ...e, positionPx, stackOffset }
})
})
</script>
</code></pre>

*Ejemplo de uso:*

<pre><code class="vue">
<TimelineEventList
:events="[
{
incidenceId: 52055284,
typeId: 3,
minutes: 68,
playerName: 'Konrad Laimer',
card: 'Yellow',
teamId: 617,
},
{
incidenceId: 52055430,
typeId: 9,
minutes: 77,
playerName: 'Désiré Doué',
assistanceBy: 'João Pedro Gonçalves Neves',
teamId: 1370,
},
{
incidenceId: 52054580,
typeId: 7,
minutes: 33,
offName: 'Josip Stanisic',
inName: 'Sacha Boey',
teamId: 617,
},
]"
side="home"
:homeTeamId="1370"
:matchDuration="95"
:totalWidthPx="700"
@event-click="handleEventClick"
/>
</code></pre>

---

h2. 3.
*Archivo:* %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}TimelineTrack.vue% — Barra Central con Pitidos

*Responsabilidad:* Renderiza la línea horizontal central y los pitidos de árbitro (final de primera parte, tiempo completo, etc.).

*Referencia (Bootstrap 5):*

<pre><code class="html">
3px;font-weight:bold;}src/composables/match/useMatchEvents.js%
<div class="position-relative w-100 bg-secondary-subtle rounded-pill" style="height: 4px;">
<div class="position-absolute bg-dark h-100" style="width: 100%;"></div>
<!-- Marcador de pitido -->
<div class="position-absolute translate-middle-x" style="left: 336px; top: -10px;">
<i class="bi bi-megaphone-fill text-muted small"></i>
</div>
</div>
</code></pre>

h3. Props

|_. Prop |_. Tipo |_. Requerido |_. Default |_. Descripción |
|@whistles@|@Array@|No|@[]@|@[{ id, minute, extraTime, periodLabel }]@|
|@matchDuration@|@Number@|No|@90@|Para calcular posición de pitidos|
|@totalWidthPx@|@Number@|No|@700@|Ancho de la pista en px|

h3. Implementación

<pre><code class="vue">
<!-- src/components/sports/football/specific/TimelineTrack.vue -->
<template>
<div class="timeline-track position-relative w-100 my-2">
<div class="track-line bg-dark-subtle position-absolute w-100" style="height: 2px; top: 50%" />
<WhistleTimeCard
v-for="whistle in enrichedWhistles"
:key="whistle.incidenceId"
:event="whistle"
:positionPx="whistle.positionPx"
@item-click="$emit('whistle-click', $event)"
/>
</div>
</template>

<script setup>
import { computed } from 'vue'
import WhistleTimeCard from './WhistleTimeCard.vue'

const props = defineProps({
whistles: { type: Array, default: () => [] },
matchDuration: { type: Number, default: 90 },
totalWidthPx: { type: Number, default: 700 },
})
defineEmits(['whistle-click'])

const enrichedWhistles = computed(() =>
props.whistles.map((w) => ({
...w,
positionPx: (w.minutes / props.matchDuration)
* props.totalWidthPx,
})),
)
</script>
</code></pre>

*Ejemplo de uso:*

<pre><code class="vue">
<!-- Pitido de inicio de partido -->
<TimelineTrack
:whistles="[{ incidenceId: 52054099, minutes: 0, type: 'Begin of match' }]"
:matchDuration="103"
:totalWidthPx="700"
/>
</code></pre>

---

h2. 4.
*Función:* Realizar el ordenamiento cronológico ( %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}MatchClock.vue% — Estado del Reloj del Partido

*Responsabilidad:* Muestra la etiqueta del período actual (TC, MT, minuto en curso) de forma concisa.

*Referencia (Bootstrap 5):*

<pre><code class="html">
<div class="d-flex align-items-center gap-2">
<div class="badge bg-dark rounded-circle p-2 fs-6">
<span title="Tiempo completo">TC</span>
</div>
</div>
</code></pre>

h3. Props

|_. Prop |_. Tipo |_. Requerido |_. Default |_. Descripción |
|@period@|@String@|Sí|—|@'FullTime'@ \|@'HalfTime'@ \|@'InProgress'@ \|@'PreMatch'@|
|@minute@|@Number@|No|@null@|Minuto actual (solo cuando @period === 'InProgress'@)|
|@extraTime@|@Number@|No|@0@|Tiempo añadido actual|

h3. Mapeo de período a etiqueta

|_. @period@ |_. Abreviatura |_. Título (@abbr@) |
|@'FullTime'@|@TC@|Tiempo Completo|
|@'HalfTime'@|@MT@|Medio Tiempo|
|@'InProgress'@|@{minute}'@ ó @{minute}+{extra}'@|En curso|
|@'PreMatch'@|No muestra reloj|—|

h3. Implementación

<pre><code class="vue">
<!-- src/components/sports/football/specific/MatchClock.vue -->
<template>
<div v-if="clockLabel" class="match-clock d-flex align-items-center">
<span
class="badge bg-dark rounded-circle fw-bold d-flex align-items-center justify-content-center"
style="width: 32px; height: 32px; font-size: 0.75rem"
:title="clockTitle"
>
{{ clockLabel }}
</span>
</div>
</template>

<script setup>
import { computed } from 'vue'
import { formatMatchTime } from '@/utils/datetime/formatMatchTime'

const props = defineProps({
period: { type: String, required: true },
minute: { type: Number, default: null },
extraTime: { type: Number, default: 0 },
})

const PERIOD_CONFIG = {
FullTime: { label: 'TC', title: 'Tiempo Completo' },
HalfTime: { label: 'MT', title: 'Medio Tiempo' },
InProgress: { label: null, title: 'En curso' },
PreMatch: { label: null, title: '' },
}

const clockLabel = computed(() => {
if (props.period === 'InProgress' && props.minutes != null) {
return formatMatchTime(props.minutes)
}
return PERIOD_CONFIG[props.period]?.label ?? null
})

const clockTitle = computed(() => PERIOD_CONFIG[props.period]?.title ?? '')
</script>
</code></pre>

*Ejemplos de uso:*

<pre><code class="vue">
<!-- Partido terminado -->
<MatchClock period="FullTime" />
<!-- Muestra: TC (con abbr "Tiempo Completo") -->

<!-- En curso, minuto 67 -->
<abbr :title="clockTitle">67'</abbr>
</code></pre>

---

h2. 5.
3px;font-weight:bold;}minute% + %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}TimelineBar.vue% — Orquestador Principal 3px;font-weight:bold;}extra_minute% ) una sola vez al inicio.

*Responsabilidad:* Ensambla todos los sub-componentes: escudos de equipo, listas de eventos home/away, la pista central y el reloj.

*Referencia (Bootstrap 5):*

<pre><code class="html">
<div class="container-fluid py-3 shadow-sm bg-light rounded d-flex align-items-center gap-3">
<!-- Escudos -->
<div class="d-flex flex-column gap-1">
<img src="home.png" class="img-thumbnail rounded-circle" style="width: 24px" />
<img src="away.png" class="img-thumbnail rounded-circle" style="width: 24px" />
</div>

<!-- Barra -->
<div class="flex-grow-1 position-relative" style="height: 60px">
<!-- Events Home -->
<ul class="position-absolute w-100 list-unstyled" style="bottom: 30px">
...
</ul>
<!-- Pista -->
<div class="position-absolute w-100 bg-secondary" style="height: 2px; top: 29px"></div>
<!-- Events Away -->
<ul class="position-absolute w-100 list-unstyled" style="top: 30px">
...
</ul>
</div>

<!-- Reloj -->
<div class="ms-auto badge bg-dark p-2">TC</div>
</div>
</code></pre>

h3. Props B. Renderizado Dinámico

|_. Prop |_. Tipo |_. Requerido |_. Default |_. Descripción |
|@homeTeam@|@Object@|Sí|—|@{ teamId, name, teamType }@ para los escudos|
|@awayTeam@|@Object@|Sí|—|igual|
|@homeEvents@|@Array<MatchEvent>@|Sí|@[]@|Eventos del equipo local|
|@awayEvents@|@Array<MatchEvent>@|Sí|@[]@|Eventos del equipo visitante|
|@whistleEvents@|@Array@|No|@[]@|Pitidos
Para cumplir con el estándar de árbitro para la pista central|
|@matchPeriod@|@String@|Sí|—|@'FullTime'@ \|@'HalfTime'@ \|@'InProgress'@ \|@'PreMatch'@|
|@currentMinute@|@Number@|No|@null@|Minuto actual (si está
*Propósito Único*, el timeline debe decidir qué componente específico mostrar basándose en curso)|
|@matchDuration@|@Number@|No|@90@|Para escalar posiciones|
|@extraTimeDuration@|@Number@|No|@0@|Minutos
el tipo de tiempo añadido totales| incidencia:

h3. Implementación

<pre><code class="vue">
<!-- src/components/sports/football/general/TimelineBar.vue -->
<template>
<div class="timeline-bar d-flex align-items-center p-3 bg-white rounded shadow-sm gap-3"> class=&quot;df-widgets match-timeline&quot;&gt;
<!-- Escudos a los lados de la barra -->
<div class="team-thumbs d-flex flex-column gap-2 border-end pe-3">
&lt;component
:is=&quot;getEventComponent(event.type)&quot;
v-for=&quot;event in sortedEvents&quot;
:key=&quot;event.id&quot;

<div class="thumb home">
<TeamImage :teamId="homeTeam.teamId" :teamType="homeTeam.teamType" :imgSize="24" />
</div>
<div class="thumb away">
<TeamImage :teamId="awayTeam.teamId" :teamType="awayTeam.teamType" :imgSize="24" />
</div>
:event-data=&quot;event&quot;
</div>

<!-- Área de eventos -->
<div class="timeline-content flex-grow-1 position-relative" style="height: 80px" ref="barRef">
<TimelineEventList
:events="homeEvents"
side="home"
:homeTeamId="homeTeam.teamId"
:matchDuration="effectiveDuration"
:totalWidthPx="barWidth"
@event-click="$emit('event-click', $event)"
/>
<TimelineTrack
:whistles="whistleEvents"
:matchDuration="effectiveDuration"
:totalWidthPx="barWidth"
/>
<TimelineEventList
:events="awayEvents"
side="away"
:homeTeamId="homeTeam.teamId"
:matchDuration="effectiveDuration"
:totalWidthPx="barWidth"
@event-click="$emit('event-click', $event)"
/>
</div>

<!-- Reloj del partido -->
<div class="border-start ps-3">
<MatchClock :period="matchPeriod" :minutes="currentMinutes" />
</div>

</div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted computed } from 'vue' &#x27;vue&#x27;;
import TeamImage GoalTimeCard from '@/components/sports/TeamImage.vue' &#x27;../specific/GoalTimeCard.vue&#x27;;
import TimelineEventList CardTimeCard from '../specific/TimelineEventList.vue' &#x27;../specific/CardTimeCard.vue&#x27;;
import TimelineTrack from '../specific/TimelineTrack.vue'
import MatchClock from '../specific/MatchClock.vue'
// ... otros componentes específicos

const props = defineProps({
homeTeam: { type: Object, required: true },
awayTeam: { type: Object, required: true },
homeEvents: { type: Array, default: () => [] },
awayEvents: { type: Array, default: () => [] },
whistleEvents: { type: Array, default: () => [] },
matchPeriod: { type: String, required: true },
currentMinute: { type: Number, default: null },
matchDuration: { type: Number, default: 90 },
extraTimeDuration: { type: Number, default: 0 },
})
defineEmits(['event-click'])

// Ancho real Lógica para determinar el componente de la barra — se mide con ResizeObserver negocio específico
const barRef getEventComponent = ref(null)
const barWidth = ref(700)

const effectiveDuration = computed(()
(type) => props.matchDuration + props.extraTimeDuration)

let observer
onMounted(() =>
{
observer const components = new ResizeObserver(([entry]) => {
barWidth.value = entry.contentRect.width GOAL: GoalTimeCard,
CARD: CardTimeCard,
// El nombre del específico termina con el base que extiende (TimeCard)

}) };
if (barRef.value) observer.observe(barRef.value) return components[type];
}) };
onUnmounted(() => observer?.disconnect())
</script>
</code></pre>

*Ejemplo h2. 3. Estándares de uso:* Código Aplicados

<pre><code class="vue"> * *Props:* El widget debe recibir el objeto de configuración inicial con tipos definidos y valores por defecto.
<TimelineBar
:homeTeam="{ teamId: 9319, name: 'Deportivo Riestra', teamType: 'club' }"
:awayTeam="{ teamId: 14236, name: 'Deportivo Maipú', teamType: 'club' }"
:homeEvents="homeEvents"
:awayEvents="awayEvents"
:whistleEvents="[{ id: 'w1', minute: 45, extraTime: 5, periodLabel: '45+5\'' }]"
matchPeriod="FullTime"
:matchDuration="90"
:extraTimeDuration="5"
@event-click="openEventDetail"
/>
</code></pre>

---

h2. 6. %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}TimelineWidget.vue% — Entry Point
* *Emits:* Cualquier interacción (ej: clic en un jugador del Widget

*Responsabilidad:* Widget autocontenido. Carga los datos del partido desde la API o store, los normaliza y los pasa a
timeline) debe emitirse hacia arriba usando %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}MatchHeader% y 3px;font-weight:bold;}kebab-case% (ej: %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}TimelineBar% . Gestiona estados 3px;font-weight:bold;}player-selected% ).
* *Estilos:* Usar clases semánticas. No incrustar tokens
de carga y error.

*Referencia HTML:* Estructura completa de %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}docs/examples/timeline.html%

h3. Props

|_. Prop |_. Tipo |_. Requerido |_. Default |_. Descripción |
|@matchId@|`Number\|String`|Sí|—|ID
diseño específicos del partido|
|@lang@|@String@|No|@'es'@|Locale para fechas y textos|
|@showHeader@|@Boolean@|No|@true@|Mostrar/ocultar el @MatchHeader@|

h3. Implementación

<pre><code class="vue">
<!-- src/widgets/timeline/TimelineWidget.vue -->
<template>
<ErrorBoundary>
<div class="timeline-widget">
<LoadingSpinner v-if="loading" />

<template v-else-if="matchData">
<MatchHeader v-if="showHeader" v-bind="matchHeaderProps" />
<TimelineBar v-bind="timelineBarProps" @event-click="selectedEvent = $event" />
</template>

<p v-else class="error-message">No se pudo cargar el partido.</p>
</div>
</ErrorBoundary>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import MatchHeader from '@/components/sports/football/general/MatchHeader.vue'
import TimelineBar from '@/components/sports/football/general/TimelineBar.vue'
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
import ErrorBoundary from '@/components/common/ErrorBoundary.vue'
import { useMatchData } from '@/composables/useMatchData'

const props = defineProps({
matchId: { type: [Number, String], required: true },
lang: { type: String, default: 'es' },
showHeader: { type: Boolean, default: true },
})

const { matchData, loading, fetchMatch } = useMatchData()
const selectedEvent = ref(null)

onMounted(() => fetchMatch(props.matchId))

const matchHeaderProps = computed(() => ({
homeTeam: matchData.value?.homeTeam,
awayTeam: matchData.value?.awayTeam,
homeScore: matchData.value?.homeScore,
awayScore: matchData.value?.awayScore,
competition: matchData.value?.competition,
matchDate: matchData.value?.matchDate,
stadium: matchData.value?.stadium,
referee: matchData.value?.referee,
dateLocale: props.lang,
}))

const timelineBarProps = computed(() => ({
homeTeam: matchData.value?.homeTeam,
awayTeam: matchData.value?.awayTeam,
homeEvents: matchData.value?.homeEvents ?? [],
awayEvents: matchData.value?.awayEvents ?? [],
whistleEvents: matchData.value?.whistleEvents ?? [],
matchPeriod: matchData.value?.period,
currentMinute: matchData.value?.currentMinute,
matchDuration: matchData.value?.matchDuration ?? 90,
}))
</script>
</code></pre>

---

h2. 7. Modelo
dominio dentro de Datos — los componentes %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}MatchEvent%

<pre><code class="js">
// Estructura normalizada de evento
3px;font-weight:bold;}base% que consumen los *TimeCard y TimelineEventList
{
incidenceId: Number, // Ej: 52055430
typeId: Number, // 9 (Goal), 7 (Sub), 3 (Yellow Card), 5 (Red Card), 568 (VAR)
minutes: Number, // Minuto del partido (ej: 77)
teamId: Number, // ID del equipo

// Campos por tipo (mapeados directo del JSON de producción):
playerName: String, // goal, card
assistanceBy: String, // solo goal
card: String, // 'Yellow' | 'Red' — solo card
reason: String, // card, var
offName: String, // solo substitution
inName: String, // solo substitution
type: String, // whistle (tipo de pitido) o VAR status
}
</code></pre>
este widget utiliza.

---

h2. 8. 4. Criterios de Calidad Aceptación

* El archivo %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}TimelineWidget% muestra %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}LoadingSpinner% mientras carga y usa %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}ErrorBoundary% para errores.
* %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}TimelineBar%
3px;font-weight:bold;}MatchTimeline.vue% no conoce el %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}matchId% — solo recibe datos procesados. excede las 200 líneas.
* %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}TimelineEventList% es el *único responsable* La lógica de calcular %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}positionPx% filtrado y ordenamiento reside en %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}stackOffset% 3px;font-weight:bold;}useMatchEvents.js% .
* %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}MatchClock% y %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}TimelineTrack% son Los componentes de solo presentación (< 80 líneas).
* Todos los componentes tienen historia Storybook con datos de
hijos utilizados pertenecen a %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}docs/examples/timeline.html% 3px;font-weight:bold;}components/sports/football/specific/% .

---

h2. 9. Orden de Implementación

# ✅ Completar tarea %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}timeline-items.md%

# ✅ Crear * Se utiliza la nomenclatura %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}MatchClock.vue%
# ✅ Crear %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}TimelineTrack.vue%
# ✅ Crear %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}TimelineEventList.vue%
# ✅ Crear %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}TimelineBar.vue%
# ✅ Crear %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}TimelineWidget.vue%
# ✅ Crear %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}src/widgets/timeline/timeline-widget.js%
# ✅ Agregar historias Storybook
3px;font-weight:bold;}PascalCase% para %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}TimelineWidget% el archivo y %{font-size: 0.85em;padding: 0.2em 0.4em;background-color: #656c7633;border-radius: 3px;font-weight:bold;}TimelineBar%

---
3px;font-weight:bold;}camelCase% para variables internas.

Atrás