|
1 | | -import { |
2 | | - useState, |
3 | | - type CSSProperties, |
4 | | - type PointerEvent as ReactPointerEvent, |
5 | | -} from 'react'; |
| 1 | +import { useState, type KeyboardEvent as ReactKeyboardEvent } from 'react'; |
6 | 2 | import { useLang, useSite, useVersion, withBase } from '@rspress/core/runtime'; |
7 | 3 | import { |
8 | 4 | InnerLine, |
@@ -313,6 +309,8 @@ const runtimeLayers = [ |
313 | 309 | tags: string[]; |
314 | 310 | }>; |
315 | 311 |
|
| 312 | +type RuntimeLayer = (typeof runtimeLayers)[number]; |
| 313 | + |
316 | 314 | const copy = { |
317 | 315 | zh: { |
318 | 316 | eyebrow: 'OPEN SOURCE · EMBEDDABLE AGENT RUNTIME', |
@@ -365,8 +363,9 @@ const copy = { |
365 | 363 | boundaryContract: 'API 与事件', |
366 | 364 | boundaryHostLabel: '你的应用', |
367 | 365 | boundaryHostRole: '账号、权限与界面', |
368 | | - stackTitle: 'A3S CODE / 分层图', |
369 | | - stackHint: '移入、点击或使用键盘', |
| 366 | + stackTitle: 'A3S CODE / 分层检查器', |
| 367 | + stackHint: '悬停预览 · 点击固定', |
| 368 | + stackHintMobile: '点击展开', |
370 | 369 | stackTop: '产品', |
371 | 370 | stackBottom: '记录', |
372 | 371 | tutorialStep: '步骤', |
@@ -432,8 +431,9 @@ const copy = { |
432 | 431 | boundaryContract: 'APIs + EVENTS', |
433 | 432 | boundaryHostLabel: 'YOUR APP', |
434 | 433 | boundaryHostRole: 'OWNS UI + ACCESS', |
435 | | - stackTitle: 'A3S CODE / RUNTIME LAYERS', |
436 | | - stackHint: 'HOVER, CLICK, OR USE THE KEYBOARD', |
| 434 | + stackTitle: 'A3S CODE / LAYER INSPECTOR', |
| 435 | + stackHint: 'HOVER TO PREVIEW · CLICK TO PIN', |
| 436 | + stackHintMobile: 'TAP TO EXPAND', |
437 | 437 | stackTop: 'PRODUCT', |
438 | 438 | stackBottom: 'RECORDS', |
439 | 439 | tutorialStep: 'STEP', |
@@ -528,93 +528,195 @@ function InstallSwitcher({ |
528 | 528 | ); |
529 | 529 | } |
530 | 530 |
|
531 | | -function RuntimeStack3D({ |
| 531 | +function RuntimeLayerDetail({ |
| 532 | + className, |
| 533 | + layer, |
| 534 | + locale, |
| 535 | +}: { |
| 536 | + className: string; |
| 537 | + layer: RuntimeLayer; |
| 538 | + locale: Locale; |
| 539 | +}) { |
| 540 | + return ( |
| 541 | + <article |
| 542 | + className={`a3s-runtime-layer-detail ${className}`} |
| 543 | + data-layer={layer.id} |
| 544 | + > |
| 545 | + <span>{layer.code}</span> |
| 546 | + <h2>{localeValue(layer.title, locale)}</h2> |
| 547 | + <p>{localeValue(layer.body, locale)}</p> |
| 548 | + <div> |
| 549 | + {layer.tags.map((tag) => ( |
| 550 | + <small key={tag}>{tag}</small> |
| 551 | + ))} |
| 552 | + </div> |
| 553 | + </article> |
| 554 | + ); |
| 555 | +} |
| 556 | + |
| 557 | +function RuntimeLayerInspector({ |
532 | 558 | labels, |
533 | 559 | locale, |
534 | 560 | }: { |
535 | 561 | labels: (typeof copy)[Locale]; |
536 | 562 | locale: Locale; |
537 | 563 | }) { |
538 | | - const [activeId, setActiveId] = useState('governance'); |
539 | | - const [tilt, setTilt] = useState({ x: 57, y: 0 }); |
540 | | - const active = |
541 | | - runtimeLayers.find((layer) => layer.id === activeId) ?? runtimeLayers[3]; |
| 564 | + const [selectedIndex, setSelectedIndex] = useState(3); |
| 565 | + const [previewIndex, setPreviewIndex] = useState<number | null>(null); |
| 566 | + const activeIndex = previewIndex ?? selectedIndex; |
| 567 | + const active = runtimeLayers[activeIndex] ?? runtimeLayers[3]; |
542 | 568 |
|
543 | | - function handlePointerMove(event: ReactPointerEvent<HTMLDivElement>) { |
544 | | - if (event.pointerType === 'touch') return; |
545 | | - const bounds = event.currentTarget.getBoundingClientRect(); |
546 | | - const x = event.clientX - bounds.left; |
547 | | - const y = event.clientY - bounds.top; |
548 | | - const normalizedX = x / bounds.width - 0.5; |
549 | | - const normalizedY = y / bounds.height - 0.5; |
550 | | - setTilt({ |
551 | | - x: 57 - normalizedY * 6, |
552 | | - y: normalizedX * 7, |
553 | | - }); |
| 569 | + function selectLayer(index: number) { |
| 570 | + setPreviewIndex(null); |
| 571 | + setSelectedIndex(index); |
| 572 | + } |
| 573 | + |
| 574 | + function handleLayerKeyDown( |
| 575 | + event: ReactKeyboardEvent<HTMLButtonElement>, |
| 576 | + index: number, |
| 577 | + ) { |
| 578 | + let nextIndex: number | undefined; |
| 579 | + |
| 580 | + if (event.key === 'ArrowDown' || event.key === 'ArrowRight') { |
| 581 | + nextIndex = (index + 1) % runtimeLayers.length; |
| 582 | + } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') { |
| 583 | + nextIndex = (index - 1 + runtimeLayers.length) % runtimeLayers.length; |
| 584 | + } else if (event.key === 'Home') { |
| 585 | + nextIndex = 0; |
| 586 | + } else if (event.key === 'End') { |
| 587 | + nextIndex = runtimeLayers.length - 1; |
| 588 | + } |
| 589 | + |
| 590 | + if (nextIndex === undefined) return; |
| 591 | + |
| 592 | + event.preventDefault(); |
| 593 | + selectLayer(nextIndex); |
| 594 | + const inspector = event.currentTarget.closest('.a3s-runtime-inspector'); |
| 595 | + inspector |
| 596 | + ?.querySelectorAll<HTMLButtonElement>('.a3s-runtime-layer-control') |
| 597 | + .item(nextIndex) |
| 598 | + .focus(); |
554 | 599 | } |
555 | 600 |
|
556 | 601 | return ( |
557 | | - <div className="a3s-runtime-stack" aria-label={labels.architectureAlt}> |
558 | | - <header className="a3s-runtime-stack-header"> |
| 602 | + <div className="a3s-runtime-inspector" aria-label={labels.architectureAlt}> |
| 603 | + <header className="a3s-runtime-inspector-header"> |
559 | 604 | <span> |
560 | 605 | <i aria-hidden="true" /> |
561 | 606 | {labels.stackTitle} |
562 | 607 | </span> |
563 | | - <small>{labels.stackHint}</small> |
| 608 | + <small> |
| 609 | + <span className="a3s-runtime-hint-desktop">{labels.stackHint}</span> |
| 610 | + <span className="a3s-runtime-hint-mobile"> |
| 611 | + {labels.stackHintMobile} |
| 612 | + </span> |
| 613 | + <b> |
| 614 | + {String(activeIndex + 1).padStart(2, '0')} /{' '} |
| 615 | + {String(runtimeLayers.length).padStart(2, '0')} |
| 616 | + </b> |
| 617 | + </small> |
564 | 618 | </header> |
565 | | - <div |
566 | | - className="a3s-runtime-stack-stage" |
567 | | - onPointerLeave={() => setTilt({ x: 57, y: 0 })} |
568 | | - onPointerMove={handlePointerMove} |
569 | | - > |
570 | | - <span className="a3s-runtime-stack-axis a3s-runtime-stack-axis--top"> |
571 | | - {labels.stackTop} |
572 | | - </span> |
| 619 | + <div className="a3s-runtime-inspector-body"> |
573 | 620 | <div |
574 | | - className="a3s-runtime-stack-scene" |
575 | | - style={{ |
576 | | - transform: `rotateX(${tilt.x}deg) rotateZ(-28deg) rotateY(${tilt.y}deg) scale(var(--stack-scale, 1))`, |
577 | | - }} |
| 621 | + className="a3s-runtime-inspector-visual" |
| 622 | + onMouseLeave={() => setPreviewIndex(null)} |
578 | 623 | > |
579 | | - {runtimeLayers.map((layer, index) => ( |
580 | | - <button |
581 | | - aria-pressed={active.id === layer.id} |
582 | | - className={[ |
583 | | - 'a3s-runtime-stack-layer', |
584 | | - `a3s-runtime-stack-layer--${layer.id}`, |
585 | | - active.id === layer.id ? 'is-active' : '', |
586 | | - ] |
587 | | - .filter(Boolean) |
588 | | - .join(' ')} |
589 | | - key={layer.id} |
590 | | - onClick={() => setActiveId(layer.id)} |
591 | | - onFocus={() => setActiveId(layer.id)} |
592 | | - onMouseEnter={() => setActiveId(layer.id)} |
593 | | - style={ |
594 | | - { |
595 | | - '--stack-depth': `${(runtimeLayers.length - index - 1) * 38}px`, |
596 | | - } as CSSProperties |
597 | | - } |
598 | | - type="button" |
599 | | - > |
600 | | - <small>{layer.code}</small> |
601 | | - <strong>{localeValue(layer.title, locale)}</strong> |
602 | | - <span>{layer.tags.slice(0, 2).join(' · ')}</span> |
603 | | - </button> |
604 | | - ))} |
| 624 | + <div className="a3s-runtime-model-scale" aria-hidden="true"> |
| 625 | + <span>{labels.stackTop}</span> |
| 626 | + <i /> |
| 627 | + <span>{labels.stackBottom}</span> |
| 628 | + </div> |
| 629 | + <div className="a3s-runtime-layer-model"> |
| 630 | + {runtimeLayers.map((layer, index) => ( |
| 631 | + <button |
| 632 | + aria-label={`${layer.code}: ${localeValue(layer.title, locale)}`} |
| 633 | + aria-pressed={selectedIndex === index} |
| 634 | + className={[ |
| 635 | + 'a3s-runtime-model-slab', |
| 636 | + `a3s-runtime-model-slab--${layer.id}`, |
| 637 | + activeIndex === index ? 'is-active' : '', |
| 638 | + selectedIndex === index ? 'is-selected' : '', |
| 639 | + ] |
| 640 | + .filter(Boolean) |
| 641 | + .join(' ')} |
| 642 | + key={layer.id} |
| 643 | + onClick={() => selectLayer(index)} |
| 644 | + onMouseEnter={() => setPreviewIndex(index)} |
| 645 | + style={{ |
| 646 | + left: `${index * 3}px`, |
| 647 | + top: `${24 + index * 47}px`, |
| 648 | + }} |
| 649 | + tabIndex={-1} |
| 650 | + type="button" |
| 651 | + > |
| 652 | + <span> |
| 653 | + <i /> |
| 654 | + <i /> |
| 655 | + <i /> |
| 656 | + </span> |
| 657 | + </button> |
| 658 | + ))} |
| 659 | + </div> |
| 660 | + <div |
| 661 | + className="a3s-runtime-model-caption" |
| 662 | + data-layer={active.id} |
| 663 | + aria-live="polite" |
| 664 | + > |
| 665 | + <span> |
| 666 | + <i aria-hidden="true" /> |
| 667 | + {active.code} |
| 668 | + </span> |
| 669 | + <strong>{localeValue(active.title, locale)}</strong> |
| 670 | + </div> |
605 | 671 | </div> |
606 | | - <span className="a3s-runtime-stack-axis a3s-runtime-stack-axis--bottom"> |
607 | | - {labels.stackBottom} |
608 | | - </span> |
609 | | - </div> |
610 | | - <div className="a3s-runtime-stack-detail" aria-live="polite"> |
611 | | - <span>{active.code}</span> |
612 | | - <h2>{localeValue(active.title, locale)}</h2> |
613 | | - <p>{localeValue(active.body, locale)}</p> |
614 | | - <div> |
615 | | - {active.tags.map((tag) => ( |
616 | | - <small key={tag}>{tag}</small> |
617 | | - ))} |
| 672 | + <div |
| 673 | + className="a3s-runtime-inspector-panel" |
| 674 | + onMouseLeave={() => setPreviewIndex(null)} |
| 675 | + > |
| 676 | + <nav |
| 677 | + aria-label={labels.architectureAlt} |
| 678 | + className="a3s-runtime-layer-list" |
| 679 | + > |
| 680 | + {runtimeLayers.map((layer, index) => ( |
| 681 | + <div className="a3s-runtime-layer-row" key={layer.id}> |
| 682 | + <button |
| 683 | + aria-pressed={selectedIndex === index} |
| 684 | + className={[ |
| 685 | + 'a3s-runtime-layer-control', |
| 686 | + `a3s-runtime-layer-control--${layer.id}`, |
| 687 | + activeIndex === index ? 'is-active' : '', |
| 688 | + selectedIndex === index ? 'is-selected' : '', |
| 689 | + ] |
| 690 | + .filter(Boolean) |
| 691 | + .join(' ')} |
| 692 | + onClick={() => selectLayer(index)} |
| 693 | + onFocus={() => selectLayer(index)} |
| 694 | + onKeyDown={(event) => handleLayerKeyDown(event, index)} |
| 695 | + onMouseEnter={() => setPreviewIndex(index)} |
| 696 | + type="button" |
| 697 | + > |
| 698 | + <span>{String(index + 1).padStart(2, '0')}</span> |
| 699 | + <span> |
| 700 | + <small>{layer.code.replace(/^L\d+\s*\/\s*/, '')}</small> |
| 701 | + <strong>{localeValue(layer.title, locale)}</strong> |
| 702 | + </span> |
| 703 | + <i aria-hidden="true" /> |
| 704 | + </button> |
| 705 | + {activeIndex === index ? ( |
| 706 | + <RuntimeLayerDetail |
| 707 | + className="a3s-runtime-layer-detail--mobile" |
| 708 | + layer={active} |
| 709 | + locale={locale} |
| 710 | + /> |
| 711 | + ) : null} |
| 712 | + </div> |
| 713 | + ))} |
| 714 | + </nav> |
| 715 | + <RuntimeLayerDetail |
| 716 | + className="a3s-runtime-layer-detail--desktop" |
| 717 | + layer={active} |
| 718 | + locale={locale} |
| 719 | + /> |
618 | 720 | </div> |
619 | 721 | </div> |
620 | 722 | </div> |
@@ -933,7 +1035,7 @@ export function HomeLayout() { |
933 | 1035 | <InstallSwitcher labels={labels} locale={locale} /> |
934 | 1036 | </div> |
935 | 1037 | <div className="a3s-hero-visual"> |
936 | | - <RuntimeStack3D labels={labels} locale={locale} /> |
| 1038 | + <RuntimeLayerInspector labels={labels} locale={locale} /> |
937 | 1039 | </div> |
938 | 1040 | </section> |
939 | 1041 |
|
|
0 commit comments