a3s-flow 1.1.0

Durable workflow engine and Rust SDK for A3S
Documentation
import {
  removeBase,
  useLang,
  useLocation,
  useNav,
  usePage,
  useSite,
  useVersion,
} from '@rspress/core/runtime';
import {
  IconSmallMenu,
  NavTitle,
  Search,
  SocialLinks,
  SvgWrapper,
  type NavProps,
} from '@rspress/core/theme-original';
import {
  NavLangs,
  NavMenu,
  NavMenuDivider,
  NavMenuItemWithChildren,
} from '@rspress/core/dist/theme/components/Nav/NavMenu.js';
import { NavScreen } from '@rspress/core/dist/theme/components/NavScreen/index.js';
import { useNavScreen } from '@rspress/core/dist/theme/components/NavHamburger/useNavScreen.js';
import '@rspress/core/dist/theme/components/Nav/index.css';
import '@rspress/core/dist/theme/components/NavHamburger/index.css';
import { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';

const CURRENT_ONLY_ROUTES = new Set([
  'concepts/child-workflows',
  'concepts/durable-primitives',
  'concepts/workflow-dag',
  'guide/cancellation',
  'guide/retries-and-waits',
  'guide/runtime-contract',
  'guide/signals-and-hooks',
  'nodes',
  'nodes/start',
  'nodes/condition',
  'nodes/step',
  'nodes/batch',
  'nodes/wait',
  'nodes/hook',
  'nodes/signal',
  'nodes/child-operation',
  'nodes/child-workflow',
  'nodes/child-workflows',
  'nodes/continue-as-new',
  'nodes/progress',
  'nodes/complete',
  'nodes/fail',
  'nodes/cancel',
  'nodes/timeout',
  'nodes/iteration',
  'nodes/loop',
  'operations/production',
  'operations/upgrading-to-v1',
  'playground',
  'reference/agent-skill',
  'reference/cli',
  'reference/examples',
  'reference/react',
  'reference/vue',
  'runtimes/native-typescript',
]);

function labelSocialLinks(root: ParentNode, language: string) {
  root
    .querySelectorAll<HTMLAnchorElement>(
      '.rp-social-links__item[href="https://github.com/A3S-Lab/Flow"]',
    )
    .forEach((link) => {
      link.setAttribute(
        'aria-label',
        language === 'zh'
          ? '在 GitHub 上查看 A3S Flow'
          : 'View A3S Flow on GitHub',
      );
    });
}

function versionHref(
  pathname: string,
  currentVersion: string,
  targetVersion: string,
  defaultVersion: string,
  cleanUrls: boolean,
) {
  const parts = removeBase(pathname).split('/').filter(Boolean);

  if (currentVersion !== defaultVersion && parts[0] === currentVersion) {
    parts.shift();
  }
  const locale = parts[0] === 'en' ? parts[0] : '';
  const contentParts = locale ? parts.slice(1) : parts;
  const contentRoute = contentParts
    .join('/')
    .replace(/\.html$/, '')
    .replace(/\/index$/, '');

  if (
    currentVersion === defaultVersion &&
    targetVersion !== defaultVersion &&
    CURRENT_ONLY_ROUTES.has(contentRoute)
  ) {
    return `/${[targetVersion, locale].filter(Boolean).join('/')}/`;
  }
  if (targetVersion !== defaultVersion) {
    parts.unshift(targetVersion);
  }
  if (parts.length === 0) return '/';
  if (parts.length === 1 && targetVersion !== defaultVersion) {
    parts.push(cleanUrls ? 'index' : 'index.html');
  }
  return `/${parts.join('/')}`;
}

function NavVersions() {
  const { pathname } = useLocation();
  const { page } = usePage();
  const { site } = useSite();
  const currentVersion = useVersion();
  const defaultVersion = site.multiVersion.default ?? '';
  const versions = site.multiVersion.versions ?? [];
  const items = versions.map((version) => ({
    text: version,
    link: versionHref(
      page.pageType === '404' ? '/' : pathname,
      currentVersion,
      version,
      defaultVersion,
      site.route?.cleanUrls ?? false,
    ),
  }));

  return items.length > 1 ? (
    <NavMenuItemWithChildren
      activeMatcher={(item) => item.text === currentVersion}
      menuItem={{ text: currentVersion, items }}
    />
  ) : null;
}

function isVisibleControl(element: HTMLElement, screen: HTMLElement) {
  const clippedGroup = element.closest<HTMLElement>(
    '.rp-nav-screen-menu-item__group, .rp-nav-screen-langs-group, .rp-nav-screen-versions-group',
  );
  if (clippedGroup && clippedGroup.getBoundingClientRect().height < 2) {
    return false;
  }

  const style = window.getComputedStyle(element);
  const bounds = element.getBoundingClientRect();
  return (
    screen.contains(element) &&
    style.display !== 'none' &&
    style.visibility !== 'hidden' &&
    bounds.width > 0 &&
    bounds.height > 0
  );
}

function NavHamburger() {
  const { closeScreen, isScreenOpen, toggleScreen } = useNavScreen();
  const language = useLang();
  const trigger = useRef<HTMLButtonElement>(null);
  const closeScreenRef = useRef(closeScreen);
  closeScreenRef.current = closeScreen;
  const openLabel = language === 'zh' ? '打开导航' : 'Open navigation';
  const closeLabel = language === 'zh' ? '关闭导航' : 'Close navigation';

  useEffect(() => {
    if (!isScreenOpen) return;

    let dispose = () => {};
    const frame = window.requestAnimationFrame(() => {
      const screen = document.querySelector<HTMLElement>(
        '.rp-nav-screen--open',
      );
      if (!screen) return;

      screen.id = 'flow-mobile-navigation';
      screen.setAttribute(
        'aria-label',
        language === 'zh' ? '站点导航' : 'Site navigation',
      );
      screen.setAttribute('aria-modal', 'true');
      screen.setAttribute('role', 'dialog');

      const enhancedControls = new Map<HTMLElement, () => void>();
      let controlIndex = 0;
      const enhanceControls = () => {
        labelSocialLinks(screen, language);
        const controls = screen.querySelectorAll<HTMLElement>(
          '.rp-nav-screen-menu-item:not(a), .rp-nav-screen-langs, .rp-nav-screen-versions',
        );

        controls.forEach((control) => {
          if (enhancedControls.has(control)) return;
          const cleanups: Array<() => void> = [];
          const group = control.nextElementSibling as HTMLElement | null;
          const groupId = `flow-mobile-navigation-group-${controlIndex++}`;
          control.setAttribute('role', 'button');
          control.tabIndex = 0;
          if (group) {
            group.id = groupId;
            control.setAttribute('aria-controls', groupId);
          }

          const syncExpanded = () => {
            const expanded =
              control.classList.contains('rp-nav-screen-menu-item--open') ||
              control.querySelector('[class*="__icon--open"]') !== null ||
              group?.classList.contains('rp-nav-screen-versions-group--open') ||
              group?.style.gridTemplateRows === '1fr';
            control.setAttribute('aria-expanded', String(Boolean(expanded)));
            group?.setAttribute('aria-hidden', String(!expanded));
            group?.toggleAttribute('inert', !expanded);
            group
              ?.querySelectorAll<HTMLAnchorElement>('a[href]')
              .forEach((link) => {
                link.tabIndex = expanded ? 0 : -1;
              });
          };
          syncExpanded();

          const syncAfterClick = () =>
            window.requestAnimationFrame(syncExpanded);
          control.addEventListener('click', syncAfterClick);
          cleanups.push(() => {
            control.removeEventListener('click', syncAfterClick);
            group?.removeAttribute('aria-hidden');
            group?.removeAttribute('inert');
            group
              ?.querySelectorAll<HTMLAnchorElement>('a[href]')
              .forEach((link) => link.removeAttribute('tabindex'));
          });

          const activateWithKeyboard = (event: KeyboardEvent) => {
            if (event.key !== 'Enter' && event.key !== ' ') return;
            event.preventDefault();
            control.click();
          };
          control.addEventListener('keydown', activateWithKeyboard);
          cleanups.push(() =>
            control.removeEventListener('keydown', activateWithKeyboard),
          );
          enhancedControls.set(control, () =>
            cleanups.forEach((cleanup) => cleanup()),
          );
        });
      };

      enhanceControls();
      const observer = new MutationObserver(enhanceControls);
      observer.observe(screen, { childList: true, subtree: true });

      const focusableControls = () =>
        Array.from(
          screen.querySelectorAll<HTMLElement>(
            'a[href], button:not([disabled]), [tabindex="0"]',
          ),
        ).filter((element) => isVisibleControl(element, screen));

      const handleKeyDown = (event: KeyboardEvent) => {
        if (event.key === 'Escape') {
          event.preventDefault();
          closeScreenRef.current();
          return;
        }
        if (event.key !== 'Tab') return;

        const focusable = focusableControls();
        if (focusable.length === 0) return;
        const first = focusable[0];
        const last = focusable[focusable.length - 1];
        if (event.shiftKey && document.activeElement === first) {
          event.preventDefault();
          last.focus();
        } else if (!event.shiftKey && document.activeElement === last) {
          event.preventDefault();
          first.focus();
        }
      };

      screen.addEventListener('keydown', handleKeyDown);
      focusableControls()[0]?.focus();
      dispose = () => {
        observer.disconnect();
        screen.removeEventListener('keydown', handleKeyDown);
        enhancedControls.forEach((cleanup) => cleanup());
      };
    });

    return () => {
      window.cancelAnimationFrame(frame);
      dispose();
      if (trigger.current && document.contains(trigger.current)) {
        trigger.current.focus();
      }
    };
  }, [isScreenOpen, language]);

  return (
    <>
      {isScreenOpen &&
        createPortal(
          <NavScreen isScreenOpen={isScreenOpen} toggleScreen={toggleScreen} />,
          document.getElementById('__rspress_modal_container')!,
        )}
      <button
        aria-controls="flow-mobile-navigation"
        aria-expanded={isScreenOpen}
        aria-label={isScreenOpen ? closeLabel : openLabel}
        className={`rp-nav-hamburger rp-nav-hamburger__sm rp-nav-hamburger__md${isScreenOpen ? ' rp-nav-hamburger--active' : ''}`}
        onClick={toggleScreen}
        ref={trigger}
        type="button"
      >
        <SvgWrapper icon={IconSmallMenu} />
      </button>
    </>
  );
}

export function Nav({
  beforeNavTitle,
  afterNavTitle,
  beforeNavMenu,
  afterNavMenu,
  navTitle,
}: NavProps) {
  const navList = useNav();
  const language = useLang();

  useEffect(() => {
    labelSocialLinks(document, language);
  }, [language]);

  useEffect(() => {
    const mobileSearch = document.querySelector<HTMLElement>(
      '.rp-search-button--mobile',
    );
    if (!mobileSearch) return;

    mobileSearch.setAttribute(
      'aria-label',
      language === 'zh' ? '搜索文档' : 'Search documentation',
    );
    mobileSearch.setAttribute('role', 'button');
    mobileSearch.tabIndex = 0;

    const activateWithKeyboard = (event: KeyboardEvent) => {
      if (event.key !== 'Enter' && event.key !== ' ') return;
      event.preventDefault();
      event.stopPropagation();
      mobileSearch.click();
    };
    mobileSearch.addEventListener('keydown', activateWithKeyboard);
    return () =>
      mobileSearch.removeEventListener('keydown', activateWithKeyboard);
  }, [language]);

  return (
    <header className="rp-nav">
      <div className="rp-nav__left">
        {beforeNavTitle}
        {navTitle ?? <NavTitle />}
        <NavMenu menuItems={navList} position="left" />
        {afterNavTitle}
      </div>
      <div className="rp-nav__right">
        {beforeNavMenu}
        <Search />
        <NavMenu menuItems={navList} position="right" />
        <div className="rp-nav__others">
          <NavMenuDivider />
          <NavLangs />
          <NavVersions />
          <SocialLinks />
        </div>
        <NavHamburger />
        {afterNavMenu}
      </div>
    </header>
  );
}