feat(web): enhance TasksView and plugin preview components with new features and styles

- Updated `TasksView` to include a title row with a "Coming soon" label and added a preview note for user guidance.
- Enhanced `DesignSystemSurface` to fetch and display design system showcases, improving visual representation in the plugin home section.
- Refactored `PreviewSurface` to pass the `inView` prop to `DesignSystemSurface`, optimizing rendering behavior.
- Improved CSS styles for tasks and design system components, ensuring a cohesive and visually appealing layout.

This update significantly enhances user experience by providing clearer information and improved visual elements in the tasks and plugin previews.
This commit is contained in:
pftom 2026-05-12 18:10:37 +08:00
parent 9f4e76d507
commit 3d6d434f11
10 changed files with 266 additions and 53 deletions

View file

@ -187,9 +187,12 @@ export function TasksView({ config, onOpenOrbitSettings }: Props) {
<header className="tasks-view__hero">
<div>
<p className="tasks-view__kicker">Automation workspace</p>
<h1 id="tasks-title" className="entry-section__title">
Tasks
</h1>
<div className="tasks-view__title-row">
<h1 id="tasks-title" className="entry-section__title">
Tasks
</h1>
<span className="tasks-view__coming-soon">Coming soon</span>
</div>
<p className="tasks-view__lede">
Tasks turn prompts into durable work: Orbit runs them, routines keep
them around, schedules decide when they fire, and live artifacts show
@ -206,6 +209,14 @@ export function TasksView({ config, onOpenOrbitSettings }: Props) {
</button>
</header>
<div className="tasks-view__preview-note" role="note">
<Icon name="orbit" size={14} />
<span>
Preview surface only. Orbit settings are available today; routines,
schedules, and live artifact wiring will land as the backend branches merge.
</span>
</div>
<div className="tasks-primitives" aria-label="Task primitives">
<PrimitiveCard
icon="orbit"

View file

@ -5,8 +5,9 @@
// referenced design system (`/api/design-systems/:slug/showcase`)
// - Tokens tab — the palette / typography / components inspector
// (`/api/design-systems/:slug/preview`)
// - DESIGN.md sidebar — the raw spec served from the plugin's own
// bundled asset (`/api/plugins/:id/asset/DESIGN.md`)
// - Plugin info sidebar — manifest metadata first, with the raw
// DESIGN.md spec included as a section underneath
// (`/api/plugins/:id/asset/DESIGN.md`)
//
// Falls back gracefully when the plugin does not reference an
// upstream design system (some bundles ship DESIGN.md only): the
@ -106,8 +107,9 @@ export function PluginDesignSystemDetail({
// When no upstream design system is referenced we still need a view
// for the iframe stage so PreviewModal has something to render. Fall
// back to a minimal placeholder that explains the spec lives in the
// sidebar; the user can still apply the plugin from the primary CTA.
// back to a minimal placeholder that explains the design spec lives
// in the plugin-info sidebar; the user can still apply the plugin
// from the primary CTA.
const views: PreviewView[] = dsRef
? [
{ id: 'showcase', label: t('ds.showcase'), html: showcaseHtml },
@ -117,7 +119,7 @@ export function PluginDesignSystemDetail({
{
id: 'spec',
label: 'Spec',
html: '<!doctype html><meta charset="utf-8"><body style="font:14px system-ui;color:#666;display:flex;align-items:center;justify-content:center;height:100vh;text-align:center;padding:0 24px;margin:0;">This plugin ships only the DESIGN.md spec — open the sidebar to read it.</body>',
html: '<!doctype html><meta charset="utf-8"><body style="font:14px system-ui;color:#666;display:flex;align-items:center;justify-content:center;height:100vh;text-align:center;padding:0 24px;margin:0;">This plugin ships only the design spec — open Plugin info to read DESIGN.md.</body>',
},
];
@ -131,22 +133,16 @@ export function PluginDesignSystemDetail({
exportTitleFor={(viewId) => `${record.title}${viewId}`}
onClose={onClose}
sidebar={{
label: t('ds.specToggle'),
label: 'Plugin info',
defaultOpen: true,
onToggle: handleSidebarToggle,
contentKey: record.id,
// DESIGN.md sits at the top so the spec is the first thing users
// read; the plugin-common metadata (workflow / context bundles /
// connectors / file paths / source provenance) stacks below in
// the same scroll container so the design-system modal carries
// the full inspector depth the scenario fallback already does.
// Design-system plugins are still plugins, so the inspector
// comes first. DESIGN.md remains available in the same sidebar,
// but as a spec section below the plugin-common metadata.
content: (
<div className="plugin-design-sidebar">
<DesignSpecView
source={specBody}
loadingLabel={t('ds.specLoading')}
/>
<div className="plugin-info-pane plugin-design-sidebar__meta">
<div className="plugin-info-pane">
<PluginMetaSections
record={record}
omit={{ description: true }}
@ -154,6 +150,16 @@ export function PluginDesignSystemDetail({
heading="Plugin info"
/>
</div>
<section className="plugin-design-sidebar__spec">
<div className="plugin-design-sidebar__spec-head">
<h3>DESIGN.md</h3>
<span>{assetPath.replace(/^\.\//, '')}</span>
</div>
<DesignSpecView
source={specBody}
loadingLabel={t('ds.specLoading')}
/>
</section>
</div>
),
}}

View file

@ -1,23 +1,90 @@
// Design-system preview surface — a stylised "X feels like X" tile.
// Design-system preview surface — showcase thumbnail with a brand-patch fallback.
//
// Design-system plugins do not ship a runnable preview entry, so we
// synthesise a brand patch from the manifest:
// - large serif headline using the bare brand label
// - three colour swatches derived deterministically from the
// plugin id (so each system gets a stable visual fingerprint)
// - subtle typographic specimen ("Aa Bb Cc") for character
//
// This mirrors the look of the design-systems gallery in the
// project view without requiring the daemon to render a real
// HTML preview for every system at home-load time.
// Most design-system plugins reference an upstream design system in
// `od.context.designSystem.ref`. When available, reuse the same
// showcase HTML as the detail modal so the home grid reads like real
// website thumbnails rather than synthetic color swatches. The fetch
// is lazy and cached to keep the 100+ design-system catalog cheap.
import { useEffect, useState } from 'react';
import type { DesignPreviewSpec } from '../preview';
import { fetchDesignSystemShowcase } from '../../../providers/registry';
import { buildSrcdoc } from '../../../runtime/srcdoc';
interface Props {
preview: DesignPreviewSpec;
inView: boolean;
}
export function DesignSystemSurface({ preview }: Props) {
const showcaseCache = new Map<string, string | null>();
const showcaseInflight = new Map<string, Promise<string | null>>();
function fetchCachedShowcase(id: string): Promise<string | null> {
const cached = showcaseCache.get(id);
if (cached !== undefined) return Promise.resolve(cached);
const existing = showcaseInflight.get(id);
if (existing) return existing;
const run = fetchDesignSystemShowcase(id).then((html) => {
showcaseCache.set(id, html);
showcaseInflight.delete(id);
return html;
});
showcaseInflight.set(id, run);
return run;
}
function useShowcaseHtml(
designSystemId: string | null,
inView: boolean,
): string | null | undefined {
const [html, setHtml] = useState<string | null | undefined>(() =>
designSystemId ? showcaseCache.get(designSystemId) : undefined,
);
useEffect(() => {
if (!designSystemId) {
setHtml(undefined);
return;
}
const cached = showcaseCache.get(designSystemId);
if (cached !== undefined) {
setHtml(cached);
return;
}
if (!inView) return;
let cancelled = false;
setHtml(null);
fetchCachedShowcase(designSystemId).then((next) => {
if (!cancelled) setHtml(next);
});
return () => {
cancelled = true;
};
}, [designSystemId, inView]);
return html;
}
export function DesignSystemSurface({ preview, inView }: Props) {
const showcaseHtml = useShowcaseHtml(preview.designSystemId, inView);
if (showcaseHtml) {
return (
<div className="plugins-home__design plugins-home__design--showcase">
<div className="plugins-home__design-showcase">
<iframe
title={`${preview.brand} showcase preview`}
sandbox="allow-scripts"
srcDoc={buildSrcdoc(showcaseHtml)}
tabIndex={-1}
aria-hidden
className="plugins-home__design-iframe"
/>
</div>
</div>
);
}
const [primary, secondary, ink] = preview.swatches;
return (
<div

View file

@ -39,7 +39,7 @@ export function PreviewSurface({ pluginId, pluginTitle, preview }: Props) {
inView={inView}
/>
) : preview.kind === 'design' ? (
<DesignSystemSurface preview={preview} />
<DesignSystemSurface preview={preview} inView={inView} />
) : (
<TextSurface pluginTitle={pluginTitle} />
)}

View file

@ -7,8 +7,8 @@
// - `html` → sandboxed iframe rendering the plugin's example
// output / preview entry (examples + scenarios that
// ship a `od.preview.entry` or `exampleOutputs[]`)
// - `design` → stylized "X feels like X" tile (design-system
// plugins, which do not ship a runnable preview)
// - `design` → design-system showcase thumbnail, falling back to
// a stylized brand patch when no showcase ref exists
// - `text` → fallback layout (other scenario plugins, atoms
// that slip through the visiblePlugins filter, …)
//
@ -55,6 +55,7 @@ export interface HtmlPreviewSpec {
export interface DesignPreviewSpec {
kind: 'design';
brand: string;
designSystemId: string | null;
swatches: string[];
}
@ -82,6 +83,10 @@ interface ExampleOutputEntry {
title?: unknown;
}
interface ContextRef {
ref?: unknown;
}
function readPreview(record: InstalledPluginRecord): PreviewBlock | null {
const od = record.manifest?.od as { preview?: unknown } | undefined;
if (!od || typeof od.preview !== 'object' || od.preview === null) return null;
@ -114,6 +119,14 @@ function isDesignSystemPlugin(record: InstalledPluginRecord): boolean {
return tags.some((t) => t.toLowerCase() === 'design-system');
}
function designSystemRef(record: InstalledPluginRecord): string | null {
const od = record.manifest?.od as
| { context?: { designSystem?: ContextRef } }
| undefined;
const ref = od?.context?.designSystem?.ref;
return typeof ref === 'string' && ref.length > 0 ? ref : null;
}
// Synthetic colour swatches derived from the plugin id so cards stay
// visually distinct without dragging in the real DESIGN.md content.
// Hue is pinned per-plugin (stable across renders) but lightness /
@ -219,6 +232,7 @@ export function inferPluginPreview(
return {
kind: 'design',
brand: brandLabel(record),
designSystemId: designSystemRef(record),
swatches: deriveSwatches(record),
};
}

View file

@ -16225,30 +16225,51 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
text-overflow: ellipsis;
white-space: nowrap;
}
/* Design-system sidebar stacks DESIGN.md prose at the top of the pane
and the plugin-info block below; the divider mirrors the dashed
separators inside the meta sections so they feel like one column. */
/* Design-system sidebar keeps the plugin inspector first, then exposes
DESIGN.md as a secondary spec section. This makes design systems read
like first-class plugins instead of a special DESIGN.md-only viewer. */
.plugin-design-sidebar {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.plugin-design-sidebar > .design-spec-pre,
.plugin-design-sidebar > .design-spec-empty {
flex: 0 0 auto;
}
.plugin-design-sidebar__meta {
.plugin-design-sidebar__spec {
border-top: 1px dashed var(--border);
padding-top: 8px;
padding: 14px 18px 24px;
background: var(--bg-panel);
}
.plugin-design-sidebar__divider {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
.plugin-design-sidebar__spec-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.plugin-design-sidebar__spec-head h3 {
margin: 0;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--text);
}
.plugin-design-sidebar__spec-head span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
color: var(--text-muted);
padding: 6px 0 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.plugin-design-sidebar__spec > .design-spec-pre,
.plugin-design-sidebar__spec > .design-spec-empty {
margin: 0;
max-height: 520px;
overflow: auto;
border: 1px solid var(--border);
border-radius: 10px;
}
/* Plugin info heading the badge-style header shown by the

View file

@ -556,7 +556,7 @@
-webkit-backdrop-filter: blur(6px);
}
/* Design-system synthesised tile */
/* Design-system showcase tile */
.plugins-home__design {
position: relative;
width: 100%;
@ -568,6 +568,12 @@
font-family: var(--serif);
overflow: hidden;
}
.plugins-home__design--showcase {
display: block;
padding: 0;
background: var(--bg-panel);
font-family: inherit;
}
.plugins-home__design::before {
content: '';
position: absolute;
@ -575,6 +581,29 @@
background: linear-gradient(180deg, transparent 60%, rgba(0, 0, 0, 0.18));
pointer-events: none;
}
.plugins-home__design--showcase::before {
z-index: 1;
background:
linear-gradient(180deg, rgba(0, 0, 0, 0) 52%, rgba(0, 0, 0, 0.18) 100%),
linear-gradient(90deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0));
}
.plugins-home__design-showcase {
position: absolute;
inset: 0;
overflow: hidden;
background: var(--bg-panel);
}
.plugins-home__design-iframe {
position: absolute;
top: 0;
left: 0;
width: 1280px;
height: 960px;
border: 0;
transform: scale(0.24);
transform-origin: 0 0;
pointer-events: none;
}
.plugins-home__design-headline {
position: relative;
font-size: 18px;

View file

@ -24,6 +24,28 @@
color: var(--accent-strong, var(--accent));
}
.tasks-view__title-row {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.tasks-view__coming-soon {
display: inline-flex;
align-items: center;
height: 22px;
padding: 0 8px;
border: 1px solid var(--amber, #b26200);
border-radius: 999px;
background: var(--amber-bg, #fff3e0);
color: var(--amber, #b26200);
font-size: 11px;
font-weight: 700;
line-height: 1;
white-space: nowrap;
}
.tasks-view__lede {
margin: 8px 0 0;
max-width: 680px;
@ -32,6 +54,25 @@
color: var(--text-muted);
}
.tasks-view__preview-note {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 10px 12px;
border: 1px solid color-mix(in srgb, var(--amber, #b26200) 22%, var(--border));
border-radius: var(--radius);
background: color-mix(in srgb, var(--amber-bg, #fff3e0) 70%, var(--bg-panel));
color: var(--text-muted);
font-size: 12px;
line-height: 1.45;
}
.tasks-view__preview-note svg {
flex: 0 0 auto;
margin-top: 1px;
color: var(--amber, #b26200);
}
.tasks-view__new,
.tasks-list__head button,
.task-detail__primary,

View file

@ -246,7 +246,7 @@ describe('PluginDetailsModal common metadata coverage', () => {
expect(html).toContain('mcp:invoke');
});
it('surfaces meta sections in the design-system sidebar under DESIGN.md', () => {
it('surfaces Plugin info first in the design-system sidebar, with DESIGN.md below', () => {
const html = render(
pluginWithMeta({
id: 'ds-with-meta',
@ -260,6 +260,9 @@ describe('PluginDetailsModal common metadata coverage', () => {
expect(html).toContain('plugin-meta-sections');
expect(html).toContain('plugin-meta-sections__heading');
expect(html).toMatch(/<h3[^>]*>Plugin info<\/h3>/);
expect(html).toContain('plugin-design-sidebar__spec');
expect(html).toContain('DESIGN.md');
expect(html.indexOf('Plugin info')).toBeLessThan(html.indexOf('DESIGN.md'));
expect(html).toContain('Workflow');
expect(html).toContain('Source');
});

View file

@ -16,6 +16,7 @@ interface MakeArgs {
title?: string;
tags?: string[];
mode?: string;
designSystemRef?: string;
preview?: Record<string, unknown>;
exampleOutputs?: Array<{ path: string; title?: string }>;
}
@ -37,6 +38,9 @@ function make(args: MakeArgs): InstalledPluginRecord {
od: {
kind: 'scenario',
...(args.mode ? { mode: args.mode } : {}),
...(args.designSystemRef
? { context: { designSystem: { ref: args.designSystemRef } } }
: {}),
...(args.preview ? { preview: args.preview } : {}),
...(args.exampleOutputs
? { useCase: { exampleOutputs: args.exampleOutputs } }
@ -123,12 +127,27 @@ describe('inferPluginPreview', () => {
expect(out.label).toBe('Weekly');
});
it('renders design-system plugins (mode signal) as design patches with stable swatches', () => {
const a = inferPluginPreview(make({ id: 'ds-a', mode: 'design-system', title: 'Airbnb' }));
const b = inferPluginPreview(make({ id: 'ds-a', mode: 'design-system', title: 'Airbnb' }));
it('renders design-system plugins (mode signal) as showcase-backed design surfaces', () => {
const a = inferPluginPreview(
make({
id: 'ds-a',
mode: 'design-system',
title: 'Airbnb',
designSystemRef: 'airbnb',
}),
);
const b = inferPluginPreview(
make({
id: 'ds-a',
mode: 'design-system',
title: 'Airbnb',
designSystemRef: 'airbnb',
}),
);
expect(a.kind).toBe('design');
if (a.kind !== 'design' || b.kind !== 'design') return;
expect(a.brand).toBe('Airbnb');
expect(a.designSystemId).toBe('airbnb');
expect(a.swatches).toHaveLength(3);
expect(a.swatches).toEqual(b.swatches);
});
@ -136,6 +155,8 @@ describe('inferPluginPreview', () => {
it('treats the design-system tag as a fallback signal when mode is missing', () => {
const out = inferPluginPreview(make({ id: 'ds-tag', tags: ['design-system'] }));
expect(out.kind).toBe('design');
if (out.kind !== 'design') return;
expect(out.designSystemId).toBeNull();
});
it('returns text fallback for plain scenario plugins without preview material', () => {