Widget Integration

Widget Integration Plan

This note defines how new widgets should integrate with the generator’s shared SCSS framework.

It is intentionally broader than the booking system. The goal is to make future widgets inherit site branding automatically, stay isolated from global component leakage, and remain overridable without per-site theme forks.

Goals

  • Keep one shared widget architecture for all sites.
  • Let widgets inherit the active site’s palette, type, radius, spacing, and motion defaults.
  • Avoid breaking widgets when global framework styles such as _button.scss change.
  • Make widget-specific overrides possible through a small alias layer instead of ad hoc selectors.
  • Keep widget markup, JS, and SCSS understandable to future agents.

Non-goals

  • Do not make each widget visually identical.
  • Do not solve branding by forking widget SCSS per site.
  • Do not push widget state styling into inline JS styles.
  • Do not rely on site-wide component selectors like .button for widget internals.

The Core Contract

Every widget should follow the same five-layer model:

  1. Site theme owns the global design tokens.
  2. The widget root maps those tokens into local --widget-* aliases.
  3. The widget root resets any global framework leakage that would break its internals.
  4. The widget rebuilds its own controls and layout explicitly from local aliases.
  5. Optional site-specific overrides target only the local alias layer.

This keeps the generator shared, while still letting each site skin the widget automatically.

1. Root Namespace

Each widget needs one stable, namespaced root class.

Examples:

  • .ql-booking-widget
  • .ql-quiz-widget
  • .ql-gallery-widget

Rules:

  • Use one root namespace per widget family.
  • Keep child classes under the same namespace.
  • Prefer ql-<widget>-* naming over generic names like .panel, .title, .actions.
  • Widget JS should mount inside that root and toggle state classes there, rather than depending on global DOM context.

2. Token Aliasing

The root widget class should translate site tokens into widget-local aliases.

The widget should consume only its local aliases for day-to-day styling.

Example:

.ql-widget {
  --widget-bg: var(--surface-card-bg, var(--bg));
  --widget-fg: var(--surface-card-fg, var(--fg));
  --widget-fg-muted: var(--fg-light, color-mix(in srgb, var(--fg) 72%, transparent));
  --widget-border: var(--surface-card-border, var(--border));
  --widget-accent: var(--accent);
  --widget-accent-ink: var(--bg);
  --widget-radius: var(--radius-card, 1rem);
  --widget-gap: var(--section-gap-tight, 0.75rem);
}

Principles:

  • Prefer shared site tokens first: --bg, --fg, --fg-light, --border, --accent, width tokens, radius tokens, spacing tokens.
  • Introduce local aliases even when they currently mirror site tokens one-to-one.
  • Keep raw fallback colors as the last fallback only.
  • If a widget needs a custom surface, derive it from site tokens rather than hardcoding a separate palette.

The booking system is already close to this pattern in _booking.scss, where .ql-booking-widget defines aliases like --booking-panel-bg, --booking-panel-border, and --booking-accent.

3. Local Leakage Reset

Widgets must not assume they are immune from shared framework selectors.

In this codebase, global button rules from _button.scss can leak height, line-height, max-width, font-size, text-transform, letter-spacing, and white-space into widget controls. That is what caused the booking button centering problem.

The fix is not to weaken the whole framework. The fix is to reset those properties locally inside the widget.

Recommended pattern:

.ql-widget {
  button,
  .button,
  [type="button"],
  [type="submit"],
  [role="button"] {
    appearance: none;
    margin: 0;
    border: 0;
    min-width: 0;
    max-width: none;
    width: auto;
    height: auto;
    padding: 0;
    line-height: 1.2;
    white-space: normal;
    text-transform: none;
    letter-spacing: inherit;
    font: inherit;
    color: inherit;
    background: none;
    box-shadow: none;
  }
}

Adjust the selector list to match the actual widget markup, but the principle stays the same:

  • reset global framework box model
  • reset inherited text treatment
  • reset background and shadow assumptions
  • then rebuild the widget’s controls explicitly

4. Explicit Control Primitives

After the local reset, every interactive primitive should declare its own layout.

Do not rely on line-height tricks for centering text. If a control may later contain icons, counters, chevrons, or two-line labels, it should use flex or grid from the start.

Recommended CTA pattern:

.ql-widget-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  min-height: 3rem;
  padding: 0.8rem 1.2rem;
  border-radius: 999px;
  background: var(--widget-accent);
  color: var(--widget-accent-ink);
}

Recommended choice/tile pattern:

.ql-widget-option {
  display: grid;
  gap: 0.3rem;
  align-content: center;
  min-height: 4rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--widget-border);
  border-radius: var(--widget-radius);
  background: var(--widget-bg);
}

Rules:

  • Use inline-flex or grid for buttons that may contain icons or multiple spans.
  • Use min-height, not fixed height, unless there is a strong reason.
  • Keep label alignment explicit with align-items, justify-content, and gap.
  • Style state with widget-owned classes like .is-selected, .is-disabled, .is-loading.

5. Layout, Width, And Responsive Rules

Widgets should inherit the site’s width system, not invent unrelated breakpoints or max-width values unless the component truly needs them.

Prefer:

  • --content-max-width
  • --content-max-width-prose
  • --content-max-width-wide
  • --section-gap-*

Example:

.ql-widget {
  width: min(100%, var(--surface-content-max-width, var(--content-max-width, 72rem)));
  margin-inline: auto;
}

Responsive rules should stay inside the widget namespace and collapse the widget’s own layout only.

6. Motion And Behaviour Hooks

If a widget wants reveal/fade/scroll motion, it should opt into the shared motion system instead of inventing a one-off animation contract.

Use the shared reveal runtime documented in the Hugo system map:

  • data-ql-reveal
  • data-ql-reveal-trigger
  • data-ql-reveal-target
  • data-ql-reveal-order
  • data-ql-reveal-duration
  • data-ql-reveal-base-delay
  • data-ql-reveal-stagger

The same pattern applies to widget state styling:

  • JS should toggle semantic state classes or attributes.
  • SCSS should style those states.
  • JS should not push presentation values inline unless layout calculation requires it.

7. Site Overrides

Per-site customisation should happen at the alias layer first.

Preferred:

.ql-booking-widget {
  --booking-accent: var(--brand-secondary, var(--accent));
  --booking-panel-bg: color-mix(in srgb, var(--bg) 92%, white);
}

Avoid:

  • deep selector overrides against widget internals
  • per-site copies of widget partials
  • styling the widget by changing unrelated global button rules

This is the main reason alias layers exist. They give sites a safe override surface.

8. Booking System Guidance

For the booking widget specifically, the intended architecture is:

  1. .ql-booking-widget aliases site tokens into --booking-*.
  2. Booking controls reset global button and .button leakage locally.
  3. Booking buttons, room cards, date tiles, and period controls rebuild their layout explicitly.
  4. The booking CTA uses inline-flex centering rather than line-height centering.
  5. Sites override only --booking-* when they want booking-specific branding.

The current booking partial is already strong on namespacing and token aliasing.

The main remaining hardening step is the local control reset, because the global button framework still leaks into booking controls from _button.scss.

The booking override surface should also include semantic tone aliases rather than hardcoded category colors in selectors. Current examples include:

  • --booking-tone-hall-hire
  • --booking-tone-learning
  • --booking-tone-consulting
  • --booking-tone-community
  • --booking-tone-calendar
  • --booking-status-error
  • --booking-status-ok

9. Widget Build Checklist

When building a new widget, check these in order:

  1. Does the widget have one namespaced root class?
  2. Does the root define local token aliases from site tokens?
  3. Does the widget locally reset global control leakage?
  4. Are buttons and tiles rebuilt with explicit flex/grid alignment?
  5. Are width and spacing derived from shared width and gap tokens?
  6. Are widget states represented by semantic classes or attributes?
  7. Does motion use the shared reveal system if needed?
  8. Can a site re-skin the widget through aliases without deep overrides?
  9. Is the widget documented in docs/ if it introduces a new framework pattern?

10. Agent Handoff Text

If another agent is working on a widget, this is the short architectural instruction to give it:

Treat the widget as a scoped subsystem inside the generator, not as an exception to the global theme. The site theme should own the base tokens. The widget root should alias them into local --widget-* variables. Inside the widget, reset global component leakage such as shared button styles, then rebuild the widget’s own controls explicitly with flex/grid and explicit spacing. Widget internals should consume only local aliases, so each site inherits branding automatically and can override the widget safely without forking shared SCSS.