smithery/neversight

ux-user-flow

Navigation and user flow patterns including routing, state management, progressive disclosure, and game progression. Use when designing multi-step flows, game phases, or navigation structures. (project)

Installation

$ npx skills add smithery/neversight --skill ux-user-flow

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/neversight · top by installs.

npx skills add smithery/neversight

Browse all from smithery/neversight

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

Allowed toolsRead, Write, Edit, Glob, Grep

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 10,466 B
  • docs SUMMARY.md 222 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

UX User Flow Skill

Navigation architecture and user journey patterns for single-page game applications. This skill covers routing, state-driven UI, and progressive disclosure.

State-Based Routing

Game State Machine

const GAME_STATES = {
  loading: 'game-loading',
  playing: 'game-playing',
  paused: 'game-paused',
  complete: 'game-complete',
  menu: 'game-menu'
};

class GameRouter {
  #state = GAME_STATES.loading;

  get state() {
    return this.#state;
  }

  set state(newState) {
    const oldState = this.#state;
    this.#state = newState;

    document.body.dataset.state = newState;

    this.dispatchEvent(new CustomEvent('state-change', {
      detail: { from: oldState, to: newState }
    }));
  }
}

CSS State Display

/* Hide all views by default */
[data-view] {
  display: none;
}

/* Show view matching state */
[data-state="game-loading"] [data-view="loading"],
[data-state="game-playing"] [data-view="playing"],
[data-state="game-menu"] [data-view="menu"] {
  display: block;
}

URL Hash Routing

class HashRouter {
  constructor() {
    window.addEventListener('hashchange', () => this.#handleRoute());
    this.#handleRoute();
  }

  #handleRoute() {
    const hash = window.location.hash.slice(1) || 'home';
    this.dispatchEvent(new CustomEvent('route', {
      detail: { route: hash }
    }));
  }

  navigate(route) {
    window.location.hash = route;
  }
}

Game Phase Navigation

Phase Progression

const PHASES = ['word', 'collision', 'mutation', 'story'];

class PhaseManager {
  #currentPhase = 'word';
  #completedPhases = new Set();

  get currentPhase() {
    return this.#currentPhase;
  }

  canAccess(phase) {
    const index = PHASES.indexOf(phase);
    if (index === 0) return true;
    const prevPhase = PHASES[index - 1];
    return this.#completedPhases.has(prevPhase);
  }

  completePhase(phase) {
    this.#completedPhases.add(phase);
    const nextIndex = PHASES.indexOf(phase) + 1;
    if (nextIndex < PHASES.length) {
      this.#currentPhase = PHASES[nextIndex];
    }
  }
}

Phase Indicator UI

<phase-indicator
  phases="word,collision,mutation,story"
  current="word">
</phase-indicator>

Multi-Step Flows

Wizard Pattern

Steps are stored as direct references during construction - NO querySelector:

class StepWizard extends HTMLElement {
  #steps = [];  // Direct element references
  #currentStep = 0;

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    // Build steps during construction, store direct references
    const stepData = ['Step 1', 'Step 2', 'Step 3'];
    stepData.forEach(title => {
      const step = document.createElement('div');
      step.className = 'step';
      step.setAttribute('part', 'step');
      step.textContent = title;
      this.#steps.push(step);  // Store direct reference
      this.shadowRoot.appendChild(step);
    });

    this.#updateView();
  }

  connectedCallback() {
    this.addEventListener('click', this);
    this.addEventListener('keydown', this);
  }

  disconnectedCallback() {
    this.removeEventListener('click', this);
    this.removeEventListener('keydown', this);
  }

  handleEvent(e) {
    // Handle navigation via events
  }

  next() {
    if (this.#currentStep < this.#steps.length - 1) {
      this.#currentStep++;
      this.#updateView();
    }
  }

  previous() {
    if (this.#currentStep > 0) {
      this.#currentStep--;
      this.#updateView();
    }
  }

  #updateView() {
    // Use direct references - never querySelector
    this.#steps.forEach((step, i) => {
      step.hidden = i !== this.#currentStep;
      step.setAttribute('aria-current', i === this.#currentStep ? 'step' : 'false');
    });
  }
}

customElements.define('step-wizard', StepWizard);

Progress Tracking

<ol class="step-progress" aria-label="Progress">
  <li data-status="complete">Create Account</li>
  <li data-status="current" aria-current="step">Choose Avatar</li>
  <li data-status="pending">Start Game</li>
</ol>

Progressive Disclosure

Reveal on Interaction

class ExpandableSection extends HTMLElement {
  handleEvent(e) {
    if (e.type === 'click') {
      const expanded = this.getAttribute('aria-expanded') === 'true';
      this.setAttribute('aria-expanded', !expanded);
      this.#content.hidden = expanded;
    }
  }
}

Conditional Rendering

render() {
  // Only show advanced options when enabled
  if (this.#showAdvanced) {
    this.#advancedSection.hidden = false;
  }

  // Unlock features based on progress
  this.#lockedFeatures.forEach(feature => {
    const isUnlocked = this.#progress >= feature.requiredProgress;
    feature.element.toggleAttribute('locked', !isUnlocked);
    feature.element.setAttribute('aria-disabled', !isUnlocked);
  });
}

Just-in-Time Help

#showHintIfNeeded() {
  const attempts = this.#failedAttempts;
  if (attempts >= 3) {
    this.#showHint('mild');
  }
  if (attempts >= 5) {
    this.#showHint('strong');
  }
}

Menu Navigation

Keyboard Navigation

handleEvent(e) {
  if (e.type === 'keydown') {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        this.#focusNext();
        break;
      case 'ArrowUp':
        e.preventDefault();
        this.#focusPrevious();
        break;
      case 'Home':
        e.preventDefault();
        this.#focusFirst();
        break;
      case 'End':
        e.preventDefault();
        this.#focusLast();
        break;
      case 'Escape':
        this.close();
        break;
    }
  }
}

Menu Structure

<nav aria-label="Game menu">
  <ul role="menu">
    <li role="none">
      <button role="menuitem" aria-current="page">Word Phase</button>
    </li>
    <li role="none">
      <button role="menuitem">Collision Phase</button>
    </li>
    <li role="none">
      <button role="menuitem" aria-disabled="true">Story Phase</button>
    </li>
  </ul>
</nav>

View Transitions

Crossfade Views

[data-view] {
  opacity: 0;
  transition: opacity 0.2s ease;
  pointer-events: none;
}

[data-view].active {
  opacity: 1;
  pointer-events: auto;
}

Slide Transitions

[data-view] {
  transform: translateX(100%);
  transition: transform 0.3s ease;
}

[data-view].active {
  transform: translateX(0);
}

[data-view].exiting {
  transform: translateX(-100%);
}

Focus Management

On View Change

Store focus target references during construction - NO querySelector:

class ViewRouter extends HTMLElement {
  #views = new Map();       // view name -> { element, focusTarget }
  #currentView = null;

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    // Build views during construction
    const viewConfigs = [
      { name: 'home', title: 'Home' },
      { name: 'game', title: 'Game' },
      { name: 'settings', title: 'Settings' }
    ];

    viewConfigs.forEach(config => {
      const view = document.createElement('div');
      view.className = 'view';
      view.setAttribute('data-view', config.name);

      // Create and store focus target reference
      const heading = document.createElement('h1');
      heading.textContent = config.title;
      heading.setAttribute('tabindex', '-1');  // Focusable but not in tab order
      view.appendChild(heading);

      // Store both view and focus target as direct references
      this.#views.set(config.name, {
        element: view,
        focusTarget: heading  // Direct reference for focus
      });

      this.shadowRoot.appendChild(view);
    });
  }

  #navigateTo(viewName) {
    // Hide current view
    if (this.#currentView) {
      this.#currentView.element.hidden = true;
    }

    // Show new view
    const view = this.#views.get(viewName);
    if (view) {
      view.element.hidden = false;
      this.#currentView = view;

      // Focus using direct reference - NO querySelector
      view.focusTarget?.focus();
    }
  }
}

Skip Links

<a href="#main-game" class="skip-link">Skip to game</a>
<a href="#word-list" class="skip-link">Skip to word list</a>

History Management

Browser Back/Forward

class HistoryManager {
  pushState(state, title, url) {
    history.pushState(state, title, url);
  }

  connectedCallback() {
    window.addEventListener('popstate', (e) => {
      if (e.state) {
        this.#restoreState(e.state);
      }
    });
  }

  #restoreState(state) {
    this.dispatchEvent(new CustomEvent('history-navigate', {
      detail: state
    }));
  }
}

Game Progression Patterns

Save Points

async saveProgress() {
  const progress = {
    phase: this.#currentPhase,
    completedWords: Array.from(this.#completedWords),
    score: this.#score,
    timestamp: Date.now()
  };

  await this.#storage.set('progress', progress);
}

async loadProgress() {
  const progress = await this.#storage.get('progress');
  if (progress) {
    this.#restoreProgress(progress);
  }
}

Session Continuity

connectedCallback() {
  // Check for saved session
  this.#checkSavedSession();
}

#checkSavedSession() {
  const saved = this.#storage.get('session');
  if (saved) {
    this.#showResumePrompt(saved);
  }
}

#showResumePrompt(session) {
  this.dispatchEvent(new CustomEvent('resume-available', {
    detail: {
      phase: session.phase,
      progress: session.progress
    }
  }));
}

Error Recovery

Graceful Degradation

async #loadPhase(phase) {
  try {
    const data = await this.#fetchPhaseData(phase);
    this.#renderPhase(data);
  } catch (error) {
    this.#showErrorRecovery({
      message: 'Failed to load phase',
      retry: () => this.#loadPhase(phase),
      fallback: () => this.#loadOfflineData(phase)
    });
  }
}

Offline Support

if (!navigator.onLine) {
  this.#showOfflineMode();
  this.#loadCachedContent();
}

window.addEventListener('online', () => {
  this.#syncProgress();
  this.#showOnlineMode();
});