Skip to content

Refactor Schedule Event Rendering #439

Description

@ayanimea

🌌 Refactor Schedule Event Rendering

Segmented Buffers + Main-Only Resize + Hierarchical Glow (Hardened)


🎯 Purpose

This PR refactors the Schedule event rendering architecture to:

  • Render prep & travel as proportional internal segments
  • Enforce strict side-by-side overlap (no stacking)
  • Apply glow only at wrapper level
  • Restrict resizing to main duration only
  • Keep prep & travel static during drag
  • Maintain proportional rendering (no artificial minimum heights)
  • Eliminate hardcoded values (all constants must use design tokens)
  • Remove motion/animation drift

This establishes the structural foundation of the Cosmic Laboratory model:

Time is spatially honest. Buffers are structural. Glow is restrained.


🧠 Canonical Data Model

🔒 Source of Truth (Non-Negotiable)

  • mainStart and mainEnd are canonical.
  • prepDuration and travelDuration are canonical.
  • renderStart and renderEnd are always derived.
  • event.start MUST equal renderStart.
  • event.end MUST equal renderEnd.
  • No logic may derive main duration from event.start.

If this rule is violated, resize logic will regress.


📦 Event Structure

extendedProps: {
  mainStart: Date,
  mainEnd: Date,
  prepDuration: number,      // minutes
  travelDuration: number     // minutes
}

🧮 Derived Values

const totalBuffer = prepDuration + travelDuration
const renderStart = new Date(mainStart.getTime() - totalBuffer * 60000)
const renderEnd   = mainEnd

🧱 Event Rendering Architecture

Single FullCalendar Event

Only ONE FC event is rendered per logical event.

It spans:

renderStart → renderEnd

Internal segmentation handles visual subdivision.


Internal Structure

<div className="fc-event-wrapper">
  {travelDuration > 0 && (
    <div
      className="event-segment event-travel"
      style={{ height: `${travelRatio}%` }}
    />
  )}

  {prepDuration > 0 && (
    <div
      className="event-segment event-prep"
      style={{ height: `${prepRatio}%` }}
    />
  )}

  <div
    className="event-segment event-main"
    style={{ height: `${mainRatio}%` }}
  >
    {title}
  </div>
</div>

Strict Proportional Ratios

const mainDuration = Math.max(
  1,
  (mainEnd.getTime() - mainStart.getTime()) / 60000
)

const totalDuration = prepDuration + travelDuration + mainDuration

const travelRatio = (travelDuration / totalDuration) * 100
const prepRatio   = (prepDuration / totalDuration) * 100
const mainRatio   = (mainDuration / totalDuration) * 100

⚠️ Constraints

  • NO minimum height
  • NO clamping
  • NO artificial padding
  • Segments may render below 3px — this is acceptable
  • Do NOT add fallback UI for small durations

🎨 Styling Constraints (Token Enforcement)

🚫 ABSOLUTE RULE

No hardcoded values are allowed in CSS.

Forbidden:

  • Inline RGBA
  • Inline px values
  • Inline border-radius
  • Inline blur
  • Inline shadow
  • Inline spacing

Required:

All values must use tokens from:

  • tokens.css
  • base.css
  • Design system variables

Wrapper Styling (Glow Applied Once)

.fc-event-wrapper {
  border-radius: var(--radius-lg);
  overflow: hidden;
  backdrop-filter: var(--event-blur);
  background:
    var(--event-glass-gradient),
    var(--event-base-surface);
  border: 1px solid var(--event-border);
  box-shadow: var(--event-shadow);
}

Glow must exist as a token:

--event-shadow: 
  var(--shadow-elevation-md),
  var(--shadow-glow-soft);

Segment Styling

.event-segment {
  width: 100%;
}

.event-travel {
  background: var(--event-travel-surface);
  opacity: var(--event-travel-opacity);
  border-bottom: 1px solid var(--event-divider);
}

.event-prep {
  background: var(--event-prep-surface);
  opacity: var(--event-prep-opacity);
  border-bottom: 1px solid var(--event-divider);
}

.event-main {
  position: relative;
}

⚠️ Prohibited

  • No glow on segments
  • No dashed borders
  • No gradient stacking
  • No additional shadow layers

Glow belongs to wrapper only.


🔄 Resize Behaviour (Main Only)

Resize Detection

const resizedFromTop =
  info.startDelta && !info.endDelta

const resizedFromBottom =
  info.endDelta && !info.startDelta

Resize Logic

function handleEventResize(info) {
  const { event } = info

  let mainStart = event.extendedProps.mainStart
  let mainEnd   = event.extendedProps.mainEnd

  if (resizedFromTop) {
    mainStart = newDate
  }

  if (resizedFromBottom) {
    mainEnd = newDate
  }

  const prep = event.extendedProps.prepDuration
  const travel = event.extendedProps.travelDuration

  const renderStart =
    new Date(mainStart.getTime() - (prep + travel) * 60000)

  const renderEnd = mainEnd

  event.setDates(renderStart, renderEnd)

  event.setExtendedProp('mainStart', mainStart)
  event.setExtendedProp('mainEnd', mainEnd)
}

Resize Constraints

  • Prep/travel durations NEVER change during resize
  • Dragging entire event moves whole block
  • No dynamic recalculation
  • No contextual inference
  • After resize, clustering must re-run

↔ Strict Side-by-Side Overlap

Column Layout

const gap = parseFloat(
  getComputedStyle(document.documentElement)
    .getPropertyValue('--event-column-gap')
)

const width = `calc((100% - ${(totalColumns - 1) * gap}px) / ${totalColumns})`

style={{
  left: `calc(${columnIndex} * (${width} + ${gap}px))`,
  width
}}

Overlap Rules

  • No vertical stacking
  • No z-index escalation
  • No hover elevation over sibling columns
  • Equal width distribution

🚫 Motion & Animation Constraints

The schedule is an instrument panel, not a marketing site.

Prohibited:

  • No scale transforms
  • No hover lift transforms
  • No brightness filters
  • No animated glow
  • No gradient shimmer
  • No motion beyond opacity/shadow ≤ 250ms

🧪 Edge Case Handling

Very Small Durations

Segments may render below 3px.
Do NOT substitute UI.
Do NOT pad.

Zero Buffers

If prep or travel = 0:

  • Do not render segment
  • Do not render divider

Zero Main Duration

Guard:

mainDuration = Math.max(1, computedMainDuration)

Synchronisation Rule

After any resize or drag:

  • mainStart
  • mainEnd
  • renderStart
  • renderEnd
    must be synchronised.

🚫 Explicit Non-Goals

This PR must NOT:

  • Modify buttons
  • Modify modals
  • Modify toolbar
  • Introduce gradients
  • Introduce new animations
  • Introduce new hardcoded colors

UI harmonisation is a separate PR.


🧠 Design Principles Enforced

  • Time is spatially honest
  • Buffers are structural
  • Glow is restrained
  • Motion is minimal
  • No artificial amplification
  • All constants use tokens
  • Predictability > decoration

Metadata

Metadata

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions