Skip to content

Commit 604f83b

Browse files
committed
docs: refine runtime layer inspector
1 parent c5625f0 commit 604f83b

2 files changed

Lines changed: 672 additions & 284 deletions

File tree

website/theme/components/HomeLayout.tsx

Lines changed: 181 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import {
2-
useState,
3-
type CSSProperties,
4-
type PointerEvent as ReactPointerEvent,
5-
} from 'react';
1+
import { useState, type KeyboardEvent as ReactKeyboardEvent } from 'react';
62
import { useLang, useSite, useVersion, withBase } from '@rspress/core/runtime';
73
import {
84
InnerLine,
@@ -313,6 +309,8 @@ const runtimeLayers = [
313309
tags: string[];
314310
}>;
315311

312+
type RuntimeLayer = (typeof runtimeLayers)[number];
313+
316314
const copy = {
317315
zh: {
318316
eyebrow: 'OPEN SOURCE · EMBEDDABLE AGENT RUNTIME',
@@ -365,8 +363,9 @@ const copy = {
365363
boundaryContract: 'API 与事件',
366364
boundaryHostLabel: '你的应用',
367365
boundaryHostRole: '账号、权限与界面',
368-
stackTitle: 'A3S CODE / 分层图',
369-
stackHint: '移入、点击或使用键盘',
366+
stackTitle: 'A3S CODE / 分层检查器',
367+
stackHint: '悬停预览 · 点击固定',
368+
stackHintMobile: '点击展开',
370369
stackTop: '产品',
371370
stackBottom: '记录',
372371
tutorialStep: '步骤',
@@ -432,8 +431,9 @@ const copy = {
432431
boundaryContract: 'APIs + EVENTS',
433432
boundaryHostLabel: 'YOUR APP',
434433
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',
437437
stackTop: 'PRODUCT',
438438
stackBottom: 'RECORDS',
439439
tutorialStep: 'STEP',
@@ -528,93 +528,195 @@ function InstallSwitcher({
528528
);
529529
}
530530

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({
532558
labels,
533559
locale,
534560
}: {
535561
labels: (typeof copy)[Locale];
536562
locale: Locale;
537563
}) {
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];
542568

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();
554599
}
555600

556601
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">
559604
<span>
560605
<i aria-hidden="true" />
561606
{labels.stackTitle}
562607
</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>
564618
</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">
573620
<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)}
578623
>
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>
605671
</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+
/>
618720
</div>
619721
</div>
620722
</div>
@@ -933,7 +1035,7 @@ export function HomeLayout() {
9331035
<InstallSwitcher labels={labels} locale={locale} />
9341036
</div>
9351037
<div className="a3s-hero-visual">
936-
<RuntimeStack3D labels={labels} locale={locale} />
1038+
<RuntimeLayerInspector labels={labels} locale={locale} />
9371039
</div>
9381040
</section>
9391041

0 commit comments

Comments
 (0)