Skip to content

Commit ec47346

Browse files
elihahah666qiongyu1999
andauthored
feat(web): composer 插件菜单 & 设计百宝箱 hover 预览 (#4000)
* feat(web): hover-preview column in the composer plugins menu Hovering (or arrow-defaulting to) a plugin in the composer "+" → Plugins flyout now opens a second-level preview column: a live preview hero (reusing the plugins-home PreviewSurface — media poster or sandboxed example iframe) plus the localized title, trust badge, description, and tags. The previewed row stays highlighted so the panel's subject is clear before the cursor lands. The Plugins flyout widens to fit the side column and reserves that width in the placement math; on narrow (contained) surfaces the preview drops below the list instead. Other submenus are unchanged. * feat(web): design-toolbox hover preview + styled detail panel Extends the plugin hover-preview to the project composer's design toolbox. Hovering a plugin entry now renders the same rich preview (ComposerPluginPreview — poster / sandboxed example iframe + title, trust badge, description, tags); every other kind (skill, MCP, connector, file, follow-up action) keeps a compact text card. The shared hover-detail panel (.plus-menu__detail) previously shipped with no CSS at all — it had no position/background/border, so the inline left/top never applied. This adds the panel styling (fixed, panelled, shadowed, viewport-clamped) plus the section-label and detail text styles, and clamps the panel's top so the taller plugin preview stays on screen. ComposerPluginPreview's column divider was de-scoped to the plugins-flyout context so it renders cleanly inside the toolbox panel too. * fix(web): drop redundant divider above design-toolbox search The toolbox head carried a border-bottom while the search box below it has its own full border, so with the flyout gap the two hairlines read as a doubled line. Drop the head border; the search box already bounds the section. * fix(web): localize plugin titles in the composer plugins list The list rendered the raw `plugin.title` (often English) while the hover preview used the localized `title_i18n`, so the same plugin showed two different-language names side by side. Render the localized title in the list too, and fold it into the search haystack so a localized-name query still matches. * feat(web): show plugin kind tag in the hover preview Adds a localized kind pill (Prototype / Slides / Image / Video / Audio / HyperFrames / Live artifact) to the plugin preview title row, reusing extractCategories — the same taxonomy that powers the home Community filter chips — so the preview names the plugin's type at a glance. Shows in both the composer plugins menu and the design-toolbox preview. * fix(web): localize the applied-plugin chip title The composer's applied-plugin chip showed the raw `record.title` (English, e.g. "Audio Jingle") while the same plugin's preset card / hover preview used the localized `title_i18n` (e.g. "音频铃声"), so the two read in different languages. Localize the chip in both surfaces: - HomeView: activeBadgeTitle now uses localizePluginTitle(locale, record). - PluginsSection: the ContextItem label (project ChatComposer chip) too. Mirrors the already-correct PluginPromptPresetCard pattern. * fix(web): localize trust badge, drop raw tag chips from preview - TrustBadge now resolves its label via i18n (pluginsView.trust.*) so Official / Trusted / Restricted follow the active locale everywhere it renders; an explicit `label` prop still overrides. - Drop the raw manifest tag chips from the plugin hover preview. Tags are author-defined English slugs with no controlled vocabulary, so they can't be reliably localized; the localized kind pill already conveys the category, so the card stays single-language. * feat(web): tag design-system plugins in the hover preview Design-system plugins aren't one of the home Community facets, so extractCategories returns nothing and they had no kind pill. Detect them by mode/tag (same heuristic as the preview classifier) and show the localized 'Design systems' label (entry.navDesignSystems). * i18n(plugins): backfill title/description translations for 22 example plugins These 22 official example manifests shipped title_i18n / description_i18n with only zh-CN + en, so in every other UI language (Korean, Japanese, …) the plugin name and description fell back to English — visible now that the composer surfaces them in the hover preview. Fill all to the full 18-locale set (zh-CN, zh-TW, ja, ko, de, fr, ru, es, pt-BR, it, vi, pl, id, nl, ar, tr, uk, en), matching the locale set used by the already-complete manifests. Brand/product/proper names and technical terms are kept verbatim; useCase.query stays English-authoritative. * fix(web): clamp design-toolbox detail panel into the viewport The left-side fallback (when the right side overflows) used rect.left - gap - detailWidth without clamping, so a row near the left edge or a pane narrower than detailWidth + gap*2 produced a negative left and pushed the position:fixed panel off-screen. Clamp the chosen left into [8, innerWidth - 8 - detailWidth] so it always degrades gracefully. (review: nettee/looper) * test(web): extract + unit-test toolbox detail panel positioning Pull the detail-panel placement out of showToolboxDetail into a pure computeToolboxDetailPosition helper and pin the narrow-pane clamp with a focused vitest regression (row near left edge / viewport narrower than the panel never yields a negative left; top clamps too). Addresses the nettee/looper review ask for a regression check. * fix(web): match plugins-flyout placement reserve to rendered width PLUS_MENU_PLUGIN_FLYOUT_WIDTH was 560 while .plus-menu__flyout--plugins renders at 466px, so getFlyoutPlacement over-reserved and medium-width panes (~664-757px side room) wrongly fell back to the contained layout, silently dropping the side-by-side preview column. Set it to 466 (with a cross-reference comment to the CSS), extract the boundary arithmetic into a pure resolveFlyoutSide helper, and pin the 466-vs-560 boundary with a vitest regression. (review: nettee/looper) * fix(web): localize trust badge tooltip + aria text title and aria-label still came from the hard-coded English meta.description, so on non-English locales the badge read as mixed-language to tooltips and screen readers. Resolve all of label, title, and aria-label from the localized tier key. (review: nettee/looper) * fix(web): update TrustBadge test for localized a11y text The existing TrustBadge spec asserted the old hard-coded English descriptions ("Open Design official", aria "…: Action plugin"), which the localization change replaced — that's why Web workspace tests went red. Keep a localized tier prefix in the accessible text when a contextual label is passed ("Official: Action plugin") and update the spec to pin the localized behavior. --------- Co-authored-by: qiongyu1999 <2694684348@qq.com>
1 parent b1c60c7 commit ec47346

35 files changed

Lines changed: 1422 additions & 111 deletions

File tree

apps/web/src/components/ChatComposer.tsx

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ import { buildVisualAnnotationAttachment, commentTargetDisplayName } from '../co
5555
import { Icon, type IconName } from "./Icon";
5656
import { SessionModeToggle } from './SessionModeToggle';
5757
import { ComposerPlusMenu } from './ComposerPlusMenu';
58+
import { ComposerPluginPreview } from './ComposerPluginPreview';
59+
import { computeToolboxDetailPosition } from './composer-detail-position';
5860
import { PluginDetailsModal } from "./PluginDetailsModal";
5961
import { PluginsSection, type PluginsSectionHandle } from "./PluginsSection";
6062
import { BUILT_IN_PETS, CUSTOM_PET_ID } from "./pet/pets";
@@ -3345,14 +3347,15 @@ function DesignToolboxPanel({
33453347
}
33463348
function showToolboxDetail(key: string, rect: DOMRect, node: ReactNode) {
33473349
cancelDetailClose();
3348-
const detailWidth = 264;
3349-
const gap = 8;
3350-
const toRight = rect.right + gap;
3351-
const left =
3352-
toRight + detailWidth > window.innerWidth - 8
3353-
? rect.left - gap - detailWidth
3354-
: toRight;
3355-
setToolboxDetail({ key, left, top: rect.top, node });
3350+
// Plugin rows render a tall visual preview; the helper clamps both axes
3351+
// into the viewport so the fixed panel never lands off-screen on a
3352+
// narrow pane (see computeToolboxDetailPosition).
3353+
const { left, top } = computeToolboxDetailPosition(
3354+
rect,
3355+
{ width: window.innerWidth, height: window.innerHeight },
3356+
{ detailWidth: 264, gap: 8, margin: 8, estimatedHeight: 340 },
3357+
);
3358+
setToolboxDetail({ key, left, top, node });
33563359
}
33573360
function scheduleToolboxDetailClose(key: string) {
33583361
cancelDetailClose();
@@ -3448,18 +3451,25 @@ function DesignToolboxPanel({
34483451
}
34493452
}}
34503453
detail={
3451-
<>
3452-
<div className="plus-menu__detail-title">{resource.title}</div>
3453-
{resource.subtitle ? (
3454-
<div className="plus-menu__detail-desc">{resource.subtitle}</div>
3455-
) : null}
3456-
<div className="plus-menu__detail-skill">
3457-
{designToolboxResourceKindLabel(resource.kind, t)}
3458-
</div>
3459-
<div className="plus-menu__detail-badge">
3460-
{active ? t('chat.designToolbox.selected') : resource.badge}
3461-
</div>
3462-
</>
3454+
// Plugin rows reuse the rich visual preview (poster /
3455+
// sandboxed example iframe + meta); every other kind keeps
3456+
// the compact text detail since it has no preview asset.
3457+
resource.kind === 'plugin' ? (
3458+
<ComposerPluginPreview record={resource.plugin} locale={locale} />
3459+
) : (
3460+
<>
3461+
<div className="plus-menu__detail-title">{resource.title}</div>
3462+
{resource.subtitle ? (
3463+
<div className="plus-menu__detail-desc">{resource.subtitle}</div>
3464+
) : null}
3465+
<div className="plus-menu__detail-skill">
3466+
{designToolboxResourceKindLabel(resource.kind, t)}
3467+
</div>
3468+
<div className="plus-menu__detail-badge">
3469+
{active ? t('chat.designToolbox.selected') : resource.badge}
3470+
</div>
3471+
</>
3472+
)
34633473
}
34643474
/>
34653475
);
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Hover preview panel for the composer "+" → Plugins flyout.
2+
//
3+
// The plugin list shows only a sparkle + name; committing to a plugin
4+
// from a bare name is a guess. This panel is the second-level surface
5+
// that fills in the gap: hovering (or arrow-defaulting to) a plugin
6+
// renders its live preview hero plus the title, a localized kind tag,
7+
// the trust badge, and the description, so the choice is informed before
8+
// the user clicks.
9+
//
10+
// It reuses the plugins-home preview stack (`inferPluginPreview` +
11+
// `PreviewSurface`) so the hero looks identical to the gallery tile,
12+
// and the localization helpers so the copy follows the active locale.
13+
14+
import { useMemo } from 'react';
15+
import type { InstalledPluginRecord } from '@open-design/contracts';
16+
import { useT } from '../i18n';
17+
import { PreviewSurface } from './plugins-home/cards/PreviewSurface';
18+
import { extractCategories } from './plugins-home/facets';
19+
import {
20+
localizePluginDescription,
21+
localizePluginTitle,
22+
} from './plugins-home/localization';
23+
import { inferPluginPreview } from './plugins-home/preview';
24+
import { TrustBadge } from './TrustBadge';
25+
26+
// Map a plugin's primary facet (from `extractCategories`) to its localized
27+
// chip label — the same taxonomy as the home Community filters, so the
28+
// preview's "kind" tag reads identically to those chips.
29+
function pluginKindLabel(
30+
slug: string | undefined,
31+
t: ReturnType<typeof useT>,
32+
): string | null {
33+
switch (slug) {
34+
case 'prototype':
35+
return t('homeHero.chip.prototype');
36+
case 'live-artifact':
37+
return t('homeHero.chip.liveArtifact');
38+
case 'deck':
39+
return t('pluginsHome.facet.slides');
40+
case 'image':
41+
return t('homeHero.chip.image');
42+
case 'video':
43+
return t('homeHero.chip.video');
44+
case 'hyperframes':
45+
return t('homeHero.chip.hyperframes');
46+
case 'audio':
47+
return t('homeHero.chip.audio');
48+
default:
49+
return null;
50+
}
51+
}
52+
53+
// Design-system plugins aren't one of the home Community facets, so
54+
// `extractCategories` returns nothing for them. Detect them the same way
55+
// the preview classifier does (mode / tag) so they still get a kind tag.
56+
function isDesignSystemRecord(record: InstalledPluginRecord): boolean {
57+
const od = record.manifest?.od as { mode?: unknown } | undefined;
58+
if (typeof od?.mode === 'string' && od.mode.toLowerCase() === 'design-system') {
59+
return true;
60+
}
61+
return (record.manifest?.tags ?? []).some(
62+
(tag) => tag.toLowerCase() === 'design-system',
63+
);
64+
}
65+
66+
export function ComposerPluginPreview({
67+
record,
68+
locale,
69+
}: {
70+
record: InstalledPluginRecord;
71+
locale: string;
72+
}) {
73+
const t = useT();
74+
const preview = useMemo(() => inferPluginPreview(record), [record]);
75+
const title = localizePluginTitle(locale, record);
76+
const description = localizePluginDescription(locale, record);
77+
const kindLabel = useMemo(() => {
78+
const facet = pluginKindLabel(extractCategories(record)[0], t);
79+
if (facet) return facet;
80+
return isDesignSystemRecord(record) ? t('entry.navDesignSystems') : null;
81+
}, [record, t]);
82+
83+
return (
84+
<div className="plus-menu__preview" data-plugin-id={record.id}>
85+
<div className="plus-menu__preview-meta">
86+
<div className="plus-menu__preview-title-row">
87+
<span className="plus-menu__preview-title" title={title}>
88+
{title}
89+
</span>
90+
{kindLabel ? (
91+
<span className="plus-menu__preview-kind">{kindLabel}</span>
92+
) : null}
93+
<TrustBadge trust={record.trust} />
94+
</div>
95+
{description ? (
96+
<p className="plus-menu__preview-desc">{description}</p>
97+
) : null}
98+
</div>
99+
<div className="plus-menu__preview-hero">
100+
{/* `eager` mounts media/iframe immediately — the panel is already a
101+
deliberate hover, so there is no off-screen cost to defer. */}
102+
<PreviewSurface
103+
pluginId={record.id}
104+
pluginTitle={title}
105+
preview={preview}
106+
eager
107+
/>
108+
</div>
109+
</div>
110+
);
111+
}

0 commit comments

Comments
 (0)