feat(web): add EntryHelpMenu and GithubStarBadge components for enhanced user support

- Introduced `EntryHelpMenu` to provide quick access to help resources, including links for GitHub issues, feature requests, and downloads, enhancing user support.
- Added `GithubStarBadge` to display the current star count for the GitHub repository, encouraging user engagement and visibility.
- Updated `EntryNavRail` to include the new help menu, improving navigation and accessibility of support options.
- Enhanced internationalization support by adding relevant translations for new components and menu items.
- Created a new CSS file for the "Use Everywhere" modal, which documents non-UI surfaces and provides a guide for users.

This update significantly improves user experience by integrating support resources directly into the application interface.
This commit is contained in:
pftom 2026-05-12 11:25:21 +08:00
parent 45760a75aa
commit 1daa60b283
26 changed files with 923 additions and 17 deletions

View file

@ -0,0 +1,120 @@
// Help launcher anchored to the bottom of the entry nav rail.
//
// Mirrors the Lovart-style "?" affordance shown in the bottom-left
// corner of the workspace: a single round button that opens a small
// popover with the four external help links we want every user to be
// one click away from — GitHub issues for help, GitHub PRs for feature
// requests, releases for the changelog, and the desktop download.
//
// The links open in a new tab (with safe `noopener` rel) and are
// labeled via the i18n dictionary so locale switching keeps the menu
// in the user's language.
import { useEffect, useRef, useState } from 'react';
import { Icon } from './Icon';
import { useT } from '../i18n';
const REPO = 'https://github.com/nexu-io/open-design';
const ISSUES_URL = `${REPO}/issues/new`;
const PRS_URL = `${REPO}/pulls`;
const RELEASES_URL = `${REPO}/releases`;
const LATEST_RELEASE_URL = `${REPO}/releases/latest`;
const ext = { target: '_blank', rel: 'noreferrer noopener' } as const;
export function EntryHelpMenu() {
const t = useT();
const [open, setOpen] = useState(false);
const wrapRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!open) return;
function onDocClick(e: MouseEvent) {
if (!wrapRef.current) return;
if (!wrapRef.current.contains(e.target as Node)) setOpen(false);
}
function onKey(e: KeyboardEvent) {
if (e.key === 'Escape') setOpen(false);
}
document.addEventListener('mousedown', onDocClick);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onDocClick);
document.removeEventListener('keydown', onKey);
};
}, [open]);
return (
<div className="entry-help-menu" ref={wrapRef}>
<button
type="button"
className="entry-nav-rail__btn entry-help-menu__trigger"
onClick={() => setOpen((v) => !v)}
aria-haspopup="menu"
aria-expanded={open}
aria-label={t('entry.helpAria')}
data-tooltip={t('entry.helpAria')}
data-testid="entry-help-trigger"
>
<Icon name="help-circle" size={18} />
</button>
{open ? (
<div
className="entry-help-popover"
role="menu"
aria-label={t('entry.helpMenuAria')}
>
<a
className="entry-help-popover__item"
href={ISSUES_URL}
{...ext}
role="menuitem"
onClick={() => setOpen(false)}
>
<span className="entry-help-popover__icon" aria-hidden>
<Icon name="comment" size={14} />
</span>
<span>{t('entry.helpGetHelp')}</span>
</a>
<a
className="entry-help-popover__item"
href={PRS_URL}
{...ext}
role="menuitem"
onClick={() => setOpen(false)}
>
<span className="entry-help-popover__icon" aria-hidden>
<Icon name="sparkles" size={14} />
</span>
<span>{t('entry.helpSubmitFeature')}</span>
</a>
<a
className="entry-help-popover__item"
href={LATEST_RELEASE_URL}
{...ext}
role="menuitem"
onClick={() => setOpen(false)}
>
<span className="entry-help-popover__icon" aria-hidden>
<Icon name="bell" size={14} />
</span>
<span>{t('entry.helpWhatsNew')}</span>
</a>
<div className="entry-help-popover__divider" aria-hidden />
<a
className="entry-help-popover__item"
href={RELEASES_URL}
{...ext}
role="menuitem"
onClick={() => setOpen(false)}
>
<span className="entry-help-popover__icon" aria-hidden>
<Icon name="download" size={14} />
</span>
<span>{t('entry.helpDownloadDesktop')}</span>
</a>
</div>
) : null}
</div>
);
}

View file

@ -2,12 +2,15 @@
//
// Renders a narrow icon-only column. The first slot is the brand
// logo (clicking navigates to home), followed by four primary
// actions (new project, home, projects, design systems). The
// rail no longer carries any footer chrome — language switching
// and other account-scoped controls live behind the floating
// settings cog in the top-right corner of the main content.
// actions (new project, home, projects, design systems). A small
// help launcher sits at the bottom and opens a popover with the
// canonical "ask for help / submit a feature / what's new / download
// desktop" external links. Language switching and other account-
// scoped controls live behind the floating settings cog in the
// top-right corner of the main content.
import type { ReactNode } from 'react';
import { EntryHelpMenu } from './EntryHelpMenu';
import { Icon } from './Icon';
import { useT } from '../i18n';
@ -22,13 +25,13 @@ interface Props {
interface NavButtonProps {
active?: boolean;
ariaLabel: string;
title: string;
tooltip: string;
onClick: () => void;
testId?: string;
children: ReactNode;
}
function NavButton({ active, ariaLabel, title, onClick, testId, children }: NavButtonProps) {
function NavButton({ active, ariaLabel, tooltip, onClick, testId, children }: NavButtonProps) {
return (
<button
type="button"
@ -36,7 +39,7 @@ function NavButton({ active, ariaLabel, title, onClick, testId, children }: NavB
onClick={onClick}
aria-label={ariaLabel}
aria-current={active ? 'page' : undefined}
title={title}
data-tooltip={tooltip}
{...(testId ? { 'data-testid': testId } : {})}
>
{children}
@ -56,7 +59,7 @@ export function EntryNavRail({ view, onViewChange, onNewProject }: Props) {
className="entry-nav-rail__logo"
onClick={() => onViewChange('home')}
aria-label={brandLabel}
title={brandLabel}
data-tooltip={brandLabel}
data-testid="entry-nav-logo"
>
<img
@ -67,8 +70,8 @@ export function EntryNavRail({ view, onViewChange, onNewProject }: Props) {
/>
</button>
<NavButton
ariaLabel="New project"
title="New project"
ariaLabel={t('entry.navNewProject')}
tooltip={t('entry.navNewProject')}
onClick={onNewProject}
testId="entry-nav-new-project"
>
@ -76,8 +79,8 @@ export function EntryNavRail({ view, onViewChange, onNewProject }: Props) {
</NavButton>
<NavButton
active={view === 'home'}
ariaLabel="Home"
title="Home"
ariaLabel={t('entry.navHome')}
tooltip={t('entry.navHome')}
onClick={() => onViewChange('home')}
testId="entry-nav-home"
>
@ -85,8 +88,8 @@ export function EntryNavRail({ view, onViewChange, onNewProject }: Props) {
</NavButton>
<NavButton
active={view === 'projects'}
ariaLabel="Projects"
title="Projects"
ariaLabel={t('entry.navProjects')}
tooltip={t('entry.navProjects')}
onClick={() => onViewChange('projects')}
testId="entry-nav-projects"
>
@ -94,14 +97,17 @@ export function EntryNavRail({ view, onViewChange, onNewProject }: Props) {
</NavButton>
<NavButton
active={view === 'design-systems'}
ariaLabel="Design systems"
title="Design systems"
ariaLabel={t('entry.navDesignSystems')}
tooltip={t('entry.navDesignSystems')}
onClick={() => onViewChange('design-systems')}
testId="entry-nav-design-systems"
>
<Icon name="palette" size={18} />
</NavButton>
</div>
<div className="entry-nav-rail__footer">
<EntryHelpMenu />
</div>
</nav>
);
}

View file

@ -30,12 +30,14 @@ import { DesignsTab } from './DesignsTab';
import { DesignSystemPreviewModal } from './DesignSystemPreviewModal';
import { DesignSystemsTab } from './DesignSystemsTab';
import { EntryNavRail, type EntryView as EntryViewKind } from './EntryNavRail';
import { GithubStarBadge } from './GithubStarBadge';
import { HomeView } from './HomeView';
import { Icon } from './Icon';
import { InlineModelSwitcher } from './InlineModelSwitcher';
import { NewProjectModal } from './NewProjectModal';
import type { CreateInput } from './NewProjectPanel';
import type { PluginLoopSubmit } from './PluginLoopHome';
import { UseEverywhereModal } from './UseEverywhereModal';
// Default scenario plugin for each project kind. The modal-based
// create flow no longer surfaces a plugin picker — every submission
@ -102,6 +104,7 @@ interface Props {
| 'execution'
| 'media'
| 'composio'
| 'integrations'
| 'language'
| 'appearance'
| 'notifications'
@ -151,8 +154,17 @@ export function EntryShell({
const [avatarMenuOpen, setAvatarMenuOpen] = useState(false);
const [languageExpanded, setLanguageExpanded] = useState(false);
const [newProjectOpen, setNewProjectOpen] = useState(false);
const [useEverywhereOpen, setUseEverywhereOpen] = useState(false);
const avatarMenuRef = useRef<HTMLDivElement | null>(null);
// The agent-handoff guide substitutes 127.0.0.1:7456 in every snippet
// with whatever URL the user actually has open, so the curl examples
// they paste into Hermes / openclaw / Cursor work without manual
// editing. Falls back to the documented default when window is
// unavailable (SSR / unit-test render).
const liveDaemonUrl =
typeof window !== 'undefined' ? window.location.origin : undefined;
function changeView(next: EntryViewKind) {
navigate({ kind: 'home', view: next });
}
@ -308,6 +320,20 @@ export function EntryShell({
</div>
) : null}
<div style={{ height: 1, background: 'var(--border-soft)', margin: '4px 6px' }} />
<button
type="button"
className="avatar-item"
onClick={() => {
setAvatarMenuOpen(false);
setUseEverywhereOpen(true);
}}
data-testid="entry-avatar-use-everywhere"
>
<span className="avatar-item-icon" aria-hidden>
<Icon name="link" size={14} />
</span>
<span>{t('entry.useEverywhereTitle')}</span>
</button>
<button
type="button"
className="avatar-item"
@ -336,6 +362,7 @@ export function EntryShell({
/>
<main className="entry-main entry-main--scroll">
<div className="entry-main__topbar">
<GithubStarBadge />
<InlineModelSwitcher
config={config}
agents={agents}
@ -347,6 +374,21 @@ export function EntryShell({
onApiModelChange={onApiModelChange}
onOpenSettings={onOpenSettings}
/>
<button
type="button"
className="use-everywhere-chip"
onClick={() => setUseEverywhereOpen(true)}
title={t('entry.useEverywhereTitle')}
aria-label={t('entry.useEverywhereAria')}
data-testid="entry-use-everywhere-button"
>
<span className="use-everywhere-chip__icon" aria-hidden>
<Icon name="link" size={13} />
</span>
<span className="use-everywhere-chip__label">
{t('entry.useEverywhereTitle')}
</span>
</button>
{avatarMenu}
</div>
<div
@ -424,6 +466,16 @@ export function EntryShell({
onOpenConnectorsTab={() => onOpenSettings('composio')}
onClose={() => setNewProjectOpen(false)}
/>
{useEverywhereOpen ? (
<UseEverywhereModal
onClose={() => setUseEverywhereOpen(false)}
onOpenSettings={() => {
setUseEverywhereOpen(false);
onOpenSettings('integrations');
}}
{...(liveDaemonUrl ? { daemonUrl: liveDaemonUrl } : {})}
/>
) : null}
</div>
);
}

View file

@ -73,7 +73,7 @@ interface Props {
onOpenLiveArtifact: (projectId: string, artifactId: string) => void;
onDeleteProject: (id: string) => void;
onChangeDefaultDesignSystem: (id: string) => void;
onOpenSettings: (section?: 'execution' | 'media' | 'composio' | 'language' | 'appearance' | 'notifications' | 'pet' | 'about') => void;
onOpenSettings: (section?: 'execution' | 'media' | 'composio' | 'integrations' | 'language' | 'appearance' | 'notifications' | 'pet' | 'about') => void;
}
const CONNECTOR_CALLBACK_MESSAGE_TYPE = 'open-design:connector-connected';

View file

@ -0,0 +1,123 @@
// Sticky "Star · <count>" pill in the entry top bar.
//
// Mirrors the marketing landing-page header (`apps/landing-page`):
// fetches `/repos/nexu-io/open-design` once on mount, formats the
// count, and renders a small CTA that opens the GitHub repo in a new
// tab. The result is cached at module scope so navigating between
// entry sub-views doesn't trigger a fresh API call, and the request
// is wrapped in a try/catch so an offline / rate-limited fetch never
// breaks the topbar layout — the pill simply falls back to "Star".
import { useEffect, useState } from 'react';
import { Icon } from './Icon';
import { useT } from '../i18n';
const REPO = 'https://github.com/nexu-io/open-design';
const API = 'https://api.github.com/repos/nexu-io/open-design';
const LS_KEY = 'open-design:gh-stars';
// One-hour soft cache — long enough to dodge GitHub's 60/hr
// unauthenticated quota when the same user reopens the app several
// times in a session, short enough that growing star counts still
// surface within a single working day.
const CACHE_TTL_MS = 60 * 60 * 1000;
type CachedStars = { count: number; ts: number };
let memoryCache: CachedStars | null = null;
function readPersistedCache(): CachedStars | null {
if (typeof window === 'undefined') return null;
try {
const raw = window.localStorage.getItem(LS_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw) as Partial<CachedStars>;
if (
typeof parsed.count !== 'number' ||
typeof parsed.ts !== 'number'
)
return null;
return { count: parsed.count, ts: parsed.ts };
} catch {
return null;
}
}
function writePersistedCache(value: CachedStars): void {
if (typeof window === 'undefined') return;
try {
window.localStorage.setItem(LS_KEY, JSON.stringify(value));
} catch {
// Quota errors are fine to swallow — the in-memory cache still
// keeps subsequent renders cheap within this tab.
}
}
function formatStars(count: number): string {
if (!Number.isFinite(count) || count <= 0) return '0';
if (count < 1000) return String(count);
return `${(count / 1000).toFixed(1).replace(/\.0$/, '')}K`;
}
export function GithubStarBadge() {
const t = useT();
const [count, setCount] = useState<number | null>(() => {
if (memoryCache) return memoryCache.count;
const persisted = readPersistedCache();
if (persisted) memoryCache = persisted;
return persisted ? persisted.count : null;
});
useEffect(() => {
const now = Date.now();
const cached = memoryCache ?? readPersistedCache();
if (cached && now - cached.ts < CACHE_TTL_MS) {
memoryCache = cached;
setCount(cached.count);
return;
}
const ctrl = new AbortController();
(async () => {
try {
const res = await fetch(API, {
headers: { Accept: 'application/vnd.github+json' },
signal: ctrl.signal,
});
if (!res.ok) return;
const data = (await res.json()) as { stargazers_count?: unknown };
if (typeof data.stargazers_count !== 'number') return;
const next: CachedStars = {
count: data.stargazers_count,
ts: Date.now(),
};
memoryCache = next;
writePersistedCache(next);
setCount(next.count);
} catch {
// Network failures and rate-limit 403s both land here. The
// pill keeps rendering its previous (or fallback) count.
}
})();
return () => ctrl.abort();
}, []);
return (
<a
className="entry-star-badge"
href={REPO}
target="_blank"
rel="noreferrer noopener"
aria-label={t('entry.githubStarAria')}
title={t('entry.githubStarTitle')}
data-testid="entry-star-badge"
>
<Icon name="github" size={13} className="entry-star-badge__icon" />
<span className="entry-star-badge__label">{t('entry.githubStarLabel')}</span>
<span className="entry-star-badge__sep" aria-hidden>
·
</span>
<span className="entry-star-badge__count" data-loading={count === null}>
{count === null ? '—' : formatStars(count)}
</span>
</a>
);
}

View file

@ -23,6 +23,7 @@ type IconName =
| 'folder'
| 'github'
| 'grid'
| 'help-circle'
| 'history'
| 'home'
| 'image'
@ -236,6 +237,14 @@ export function Icon({ name, size = 14, strokeWidth = 1.6, ...rest }: Props) {
<rect x="14" y="14" width="7" height="7" rx="1" />
</svg>
);
case 'help-circle':
return (
<svg {...common}>
<circle cx="12" cy="12" r="10" />
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
<path d="M12 17h.01" />
</svg>
);
case 'history':
return (
<svg {...common}>

View file

@ -168,6 +168,19 @@ export const ar: Dict = {
'entry.openSettingsAria': 'فتح الإعدادات',
'entry.resizeAria': 'تغيير حجم الشريط الجانبي',
'entry.loadingWorkspace': 'جاري تحميل مساحة العمل...',
'entry.navNewProject': 'مشروع جديد',
'entry.navHome': 'الرئيسية',
'entry.navProjects': 'المشاريع',
'entry.navDesignSystems': 'أنظمة التصميم',
'entry.helpAria': 'المساعدة',
'entry.helpMenuAria': 'قائمة المساعدة',
'entry.helpGetHelp': 'احصل على مساعدة على GitHub',
'entry.helpSubmitFeature': 'اقترح ميزة',
'entry.helpWhatsNew': 'الجديد',
'entry.helpDownloadDesktop': 'تنزيل تطبيق سطح المكتب',
'entry.githubStarLabel': 'نجمة',
'entry.githubStarTitle': 'انقر لمنحنا نجمة على GitHub',
'entry.githubStarAria': 'منح Open Design نجمة على GitHub',
'entry.tabImageTemplates': 'قوالب الصور',
'entry.tabVideoTemplates': 'قوالب الفيديو',
'promptTemplates.searchPlaceholder': 'بحث في القوالب...',

View file

@ -167,6 +167,21 @@ export const de: Dict = {
'entry.openSettingsAria': 'Einstellungen öffnen',
'entry.resizeAria': 'Seitenleiste skalieren',
'entry.loadingWorkspace': 'Workspace wird geladen…',
'entry.useEverywhereTitle': 'Überall verwenden',
'entry.useEverywhereAria': 'Anleitung „Überall verwenden“ öffnen (CLI, MCP, HTTP, Skills)',
'entry.navNewProject': 'Neues Projekt',
'entry.navHome': 'Start',
'entry.navProjects': 'Projekte',
'entry.navDesignSystems': 'Design-Systeme',
'entry.helpAria': 'Hilfe',
'entry.helpMenuAria': 'Hilfemenü',
'entry.helpGetHelp': 'Hilfe auf GitHub',
'entry.helpSubmitFeature': 'Feature vorschlagen',
'entry.helpWhatsNew': 'Neuigkeiten',
'entry.helpDownloadDesktop': 'Desktop-App herunterladen',
'entry.githubStarLabel': 'Star',
'entry.githubStarTitle': 'Klicken, um uns auf GitHub einen Stern zu geben',
'entry.githubStarAria': 'Open Design auf GitHub einen Stern geben',
'entry.tabImageTemplates': 'Bildvorlagen',
'entry.tabVideoTemplates': 'Videovorlagen',
'promptTemplates.searchPlaceholder': 'Templates suchen…',

View file

@ -166,6 +166,8 @@ export const en: Dict = {
'entry.openSettingsAria': 'Open settings',
'entry.resizeAria': 'Resize sidebar',
'entry.loadingWorkspace': 'Loading workspace…',
'entry.useEverywhereTitle': 'Use everywhere',
'entry.useEverywhereAria': 'Open the Use Everywhere guide (CLI, MCP, HTTP, Skills)',
'entry.navNewProject': 'New project',
'entry.navHome': 'Home',
'entry.navProjects': 'Projects',

View file

@ -167,6 +167,21 @@ export const esES: Dict = {
'entry.openSettingsAria': 'Abrir ajustes',
'entry.resizeAria': 'Redimensionar barra lateral',
'entry.loadingWorkspace': 'Cargando espacio de trabajo…',
'entry.useEverywhereTitle': 'Usar en todas partes',
'entry.useEverywhereAria': 'Abrir la guía «Usar en todas partes» (CLI, MCP, HTTP, Skills)',
'entry.navNewProject': 'Nuevo proyecto',
'entry.navHome': 'Inicio',
'entry.navProjects': 'Proyectos',
'entry.navDesignSystems': 'Sistemas de diseño',
'entry.helpAria': 'Ayuda',
'entry.helpMenuAria': 'Menú de ayuda',
'entry.helpGetHelp': 'Obtener ayuda en GitHub',
'entry.helpSubmitFeature': 'Enviar una sugerencia',
'entry.helpWhatsNew': 'Novedades',
'entry.helpDownloadDesktop': 'Descargar app de escritorio',
'entry.githubStarLabel': 'Star',
'entry.githubStarTitle': 'Haz clic para darnos una estrella en GitHub',
'entry.githubStarAria': 'Dar una estrella a Open Design en GitHub',
'entry.tabImageTemplates': 'Plantillas de imagen',
'entry.tabVideoTemplates': 'Plantillas de vídeo',
'promptTemplates.searchPlaceholder': 'Buscar plantillas…',

View file

@ -168,6 +168,19 @@ export const fa: Dict = {
'entry.openSettingsAria': 'باز کردن تنظیمات',
'entry.resizeAria': 'تغییر اندازه نوار کناری',
'entry.loadingWorkspace': 'در حال بارگذاری فضای کاری…',
'entry.navNewProject': 'پروژه جدید',
'entry.navHome': 'خانه',
'entry.navProjects': 'پروژه‌ها',
'entry.navDesignSystems': 'سیستم‌های طراحی',
'entry.helpAria': 'راهنما',
'entry.helpMenuAria': 'منوی راهنما',
'entry.helpGetHelp': 'دریافت کمک در GitHub',
'entry.helpSubmitFeature': 'پیشنهاد قابلیت',
'entry.helpWhatsNew': 'تازه‌ها',
'entry.helpDownloadDesktop': 'دانلود اپلیکیشن دسکتاپ',
'entry.githubStarLabel': 'ستاره',
'entry.githubStarTitle': 'برای ما در GitHub ستاره بگذارید',
'entry.githubStarAria': 'به Open Design در GitHub ستاره بدهید',
'promptTemplates.searchPlaceholder': 'جستجوی قالب‌ها…',
'promptTemplates.countLabel': '{n} نتیجه',
'promptTemplates.emptyImage': 'هنوز قالب پرامپت تصویر نصب نشده است.',

View file

@ -168,6 +168,21 @@ export const fr: Dict = {
'entry.openSettingsAria': 'Ouvrir les paramètres',
'entry.resizeAria': 'Redimensionner la barre latérale',
'entry.loadingWorkspace': 'Chargement de l\'espace de travail…',
'entry.useEverywhereTitle': 'Utiliser partout',
'entry.useEverywhereAria': 'Ouvrir le guide « Utiliser partout » (CLI, MCP, HTTP, Skills)',
'entry.navNewProject': 'Nouveau projet',
'entry.navHome': 'Accueil',
'entry.navProjects': 'Projets',
'entry.navDesignSystems': 'Systèmes de design',
'entry.helpAria': 'Aide',
'entry.helpMenuAria': 'Menu d\'aide',
'entry.helpGetHelp': 'Obtenir de l\'aide sur GitHub',
'entry.helpSubmitFeature': 'Proposer une fonctionnalité',
'entry.helpWhatsNew': 'Nouveautés',
'entry.helpDownloadDesktop': 'Télécharger l\'app de bureau',
'entry.githubStarLabel': 'Star',
'entry.githubStarTitle': 'Cliquez pour nous mettre une étoile sur GitHub',
'entry.githubStarAria': 'Mettre une étoile à Open Design sur GitHub',
'entry.tabImageTemplates': 'Modèles d\'image',
'entry.tabVideoTemplates': 'Modèles de vidéo',
'promptTemplates.searchPlaceholder': 'Rechercher des modèles…',

View file

@ -168,6 +168,19 @@ export const hu: Dict = {
'entry.openSettingsAria': 'Beállítások megnyitása',
'entry.resizeAria': 'Oldalsáv átméretezése',
'entry.loadingWorkspace': 'Munkaterület betöltése…',
'entry.navNewProject': 'Új projekt',
'entry.navHome': 'Kezdőlap',
'entry.navProjects': 'Projektek',
'entry.navDesignSystems': 'Tervezőrendszerek',
'entry.helpAria': 'Súgó',
'entry.helpMenuAria': 'Súgó menü',
'entry.helpGetHelp': 'Kérj segítséget a GitHubon',
'entry.helpSubmitFeature': 'Funkció javaslása',
'entry.helpWhatsNew': 'Újdonságok',
'entry.helpDownloadDesktop': 'Asztali alkalmazás letöltése',
'entry.githubStarLabel': 'Star',
'entry.githubStarTitle': 'Csillagozz meg minket a GitHubon',
'entry.githubStarAria': 'Csillagozd meg az Open Design projektet a GitHubon',
'entry.tabImageTemplates': 'Képsablonok',
'entry.tabVideoTemplates': 'Videósablonok',
'promptTemplates.searchPlaceholder': 'Sablonok keresése…',

View file

@ -260,6 +260,19 @@ export const id: Dict = {
'entry.openSettingsAria': 'Buka pengaturan',
'entry.resizeAria': 'Ubah ukuran sidebar',
'entry.loadingWorkspace': 'Memuat workspace...',
'entry.navNewProject': 'Proyek baru',
'entry.navHome': 'Beranda',
'entry.navProjects': 'Proyek',
'entry.navDesignSystems': 'Sistem desain',
'entry.helpAria': 'Bantuan',
'entry.helpMenuAria': 'Menu bantuan',
'entry.helpGetHelp': 'Dapatkan bantuan di GitHub',
'entry.helpSubmitFeature': 'Ajukan fitur',
'entry.helpWhatsNew': 'Apa yang baru',
'entry.helpDownloadDesktop': 'Unduh aplikasi desktop',
'entry.githubStarLabel': 'Star',
'entry.githubStarTitle': 'Klik untuk memberi star di GitHub',
'entry.githubStarAria': 'Beri star Open Design di GitHub',
'entry.tabImageTemplates': 'Templat gambar',
'entry.tabVideoTemplates': 'Templat video',

View file

@ -167,6 +167,21 @@ export const ja: Dict = {
'entry.openSettingsAria': '設定を開く',
'entry.resizeAria': 'サイドバーをリサイズ',
'entry.loadingWorkspace': 'ワークスペースを読み込み中…',
'entry.useEverywhereTitle': 'どこでも使う',
'entry.useEverywhereAria': '「どこでも使う」ガイドを開くCLI、MCP、HTTP、Skills',
'entry.navNewProject': '新規プロジェクト',
'entry.navHome': 'ホーム',
'entry.navProjects': 'プロジェクト',
'entry.navDesignSystems': 'デザインシステム',
'entry.helpAria': 'ヘルプ',
'entry.helpMenuAria': 'ヘルプメニュー',
'entry.helpGetHelp': 'GitHub でサポートを受ける',
'entry.helpSubmitFeature': '機能をリクエスト',
'entry.helpWhatsNew': '最新情報',
'entry.helpDownloadDesktop': 'デスクトップアプリをダウンロード',
'entry.githubStarLabel': 'Star',
'entry.githubStarTitle': 'GitHub でスターを付ける',
'entry.githubStarAria': 'GitHub で Open Design にスターを付ける',
'entry.tabImageTemplates': '画像テンプレート',
'entry.tabVideoTemplates': '動画テンプレート',
'promptTemplates.searchPlaceholder': 'テンプレートを検索…',

View file

@ -168,6 +168,21 @@ export const ko: Dict = {
'entry.openSettingsAria': '설정 열기',
'entry.resizeAria': '사이드바 크기 조절',
'entry.loadingWorkspace': '워크스페이스를 불러오는 중…',
'entry.useEverywhereTitle': '어디서나 사용',
'entry.useEverywhereAria': '‘어디서나 사용’ 가이드 열기 (CLI, MCP, HTTP, Skills)',
'entry.navNewProject': '새 프로젝트',
'entry.navHome': '홈',
'entry.navProjects': '프로젝트',
'entry.navDesignSystems': '디자인 시스템',
'entry.helpAria': '도움말',
'entry.helpMenuAria': '도움말 메뉴',
'entry.helpGetHelp': 'GitHub 에서 도움 받기',
'entry.helpSubmitFeature': '기능 제안하기',
'entry.helpWhatsNew': '새로운 소식',
'entry.helpDownloadDesktop': '데스크탑 앱 다운로드',
'entry.githubStarLabel': 'Star',
'entry.githubStarTitle': 'GitHub 에서 별을 눌러주세요',
'entry.githubStarAria': 'GitHub 에서 Open Design 에 별 누르기',
'entry.tabImageTemplates': '이미지 템플릿',
'entry.tabVideoTemplates': '비디오 템플릿',
'promptTemplates.searchPlaceholder': '템플릿 검색…',

View file

@ -168,6 +168,19 @@ export const pl: Dict = {
'entry.openSettingsAria': 'Otwórz ustawienia',
'entry.resizeAria': 'Zmień rozmiar paska bocznego',
'entry.loadingWorkspace': 'Ładowanie obszaru roboczego…',
'entry.navNewProject': 'Nowy projekt',
'entry.navHome': 'Strona główna',
'entry.navProjects': 'Projekty',
'entry.navDesignSystems': 'Systemy projektowe',
'entry.helpAria': 'Pomoc',
'entry.helpMenuAria': 'Menu pomocy',
'entry.helpGetHelp': 'Uzyskaj pomoc na GitHubie',
'entry.helpSubmitFeature': 'Zaproponuj funkcję',
'entry.helpWhatsNew': 'Co nowego',
'entry.helpDownloadDesktop': 'Pobierz aplikację na komputer',
'entry.githubStarLabel': 'Star',
'entry.githubStarTitle': 'Daj nam gwiazdkę na GitHubie',
'entry.githubStarAria': 'Daj Open Design gwiazdkę na GitHubie',
'entry.tabImageTemplates': 'Szablony obrazów',
'entry.tabVideoTemplates': 'Szablony wideo',
'promptTemplates.searchPlaceholder': 'Szukaj szablonów…',

View file

@ -165,6 +165,21 @@ export const ptBR: Dict = {
'entry.openSettingsAria': 'Abrir configurações',
'entry.resizeAria': 'Redimensionar barra lateral',
'entry.loadingWorkspace': 'Carregando área de trabalho…',
'entry.useEverywhereTitle': 'Usar em qualquer lugar',
'entry.useEverywhereAria': 'Abrir o guia “Usar em qualquer lugar” (CLI, MCP, HTTP, Skills)',
'entry.navNewProject': 'Novo projeto',
'entry.navHome': 'Início',
'entry.navProjects': 'Projetos',
'entry.navDesignSystems': 'Design systems',
'entry.helpAria': 'Ajuda',
'entry.helpMenuAria': 'Menu de ajuda',
'entry.helpGetHelp': 'Obter ajuda no GitHub',
'entry.helpSubmitFeature': 'Sugerir um recurso',
'entry.helpWhatsNew': 'Novidades',
'entry.helpDownloadDesktop': 'Baixar app para desktop',
'entry.githubStarLabel': 'Star',
'entry.githubStarTitle': 'Clique para nos dar uma estrela no GitHub',
'entry.githubStarAria': 'Dar uma estrela ao Open Design no GitHub',
'entry.tabImageTemplates': 'Modelos de imagem',
'entry.tabVideoTemplates': 'Modelos de vídeo',
'promptTemplates.searchPlaceholder': 'Buscar templates…',

View file

@ -165,6 +165,19 @@ export const ru: Dict = {
'entry.openSettingsAria': 'Открыть настройки',
'entry.resizeAria': 'Изменить размер боковой панели',
'entry.loadingWorkspace': 'Загрузка рабочего пространства…',
'entry.navNewProject': 'Новый проект',
'entry.navHome': 'Главная',
'entry.navProjects': 'Проекты',
'entry.navDesignSystems': 'Дизайн-системы',
'entry.helpAria': 'Помощь',
'entry.helpMenuAria': 'Меню помощи',
'entry.helpGetHelp': 'Получить помощь на GitHub',
'entry.helpSubmitFeature': 'Предложить функцию',
'entry.helpWhatsNew': 'Что нового',
'entry.helpDownloadDesktop': 'Скачать настольное приложение',
'entry.githubStarLabel': 'Star',
'entry.githubStarTitle': 'Поставьте нам звезду на GitHub',
'entry.githubStarAria': 'Поставить Open Design звезду на GitHub',
'entry.tabImageTemplates': 'Шаблоны изображений',
'entry.tabVideoTemplates': 'Шаблоны видео',
'promptTemplates.searchPlaceholder': 'Поиск шаблонов…',

View file

@ -162,6 +162,19 @@ export const tr: Dict = {
'entry.openSettingsAria': 'Ayarları aç',
'entry.resizeAria': 'Yan çubuğu yeniden boyutlandır',
'entry.loadingWorkspace': 'Çalışma alanı yükleniyor…',
'entry.navNewProject': 'Yeni proje',
'entry.navHome': 'Ana sayfa',
'entry.navProjects': 'Projeler',
'entry.navDesignSystems': 'Tasarım sistemleri',
'entry.helpAria': 'Yardım',
'entry.helpMenuAria': 'Yardım menüsü',
'entry.helpGetHelp': 'GitHub üzerinden yardım alın',
'entry.helpSubmitFeature': 'Özellik öner',
'entry.helpWhatsNew': 'Yenilikler',
'entry.helpDownloadDesktop': 'Masaüstü uygulamasını indir',
'entry.githubStarLabel': 'Star',
'entry.githubStarTitle': 'GitHub üzerinde bize yıldız verin',
'entry.githubStarAria': 'Open Design\u2019a GitHub üzerinde yıldız ver',
'entry.tabImageTemplates': 'Görsel istemleri',
'entry.tabVideoTemplates': 'Video istemleri',
'promptTemplates.searchPlaceholder': 'Şablon ara…',
@ -483,6 +496,24 @@ export const tr: Dict = {
'avatar.reasoningLabel': 'Akıl yürütme',
'avatar.customSuffix': '(özel)',
'inlineSwitcher.chipTitle': 'CLI / modeli değiştir',
'inlineSwitcher.chipCli': 'Yerel CLI',
'inlineSwitcher.chipByok': 'BYOK',
'inlineSwitcher.modelDefault': 'varsayılan',
'inlineSwitcher.noAgent': 'ajan yok',
'inlineSwitcher.modeLabel': 'Mod',
'inlineSwitcher.agentLabel': 'Ajan',
'inlineSwitcher.providerLabel': 'Sağlayıcı',
'inlineSwitcher.modelLabel': 'Model',
'inlineSwitcher.useCli': 'Yerel CLI kullan',
'inlineSwitcher.useByok': 'Kendi API anahtarını kullan',
'inlineSwitcher.daemonOffline': 'Daemon çevrimdışı — ayarları aç',
'inlineSwitcher.noAgentsDetected': "PATH'te CLI bulunamadı",
'inlineSwitcher.openSettingsForModel': 'Sağlayıcıyı Ayarlardan yapılandırın',
'inlineSwitcher.missingApiKey': 'API anahtarı yok — Ayarlardan ekleyin.',
'inlineSwitcher.openFullSettings': 'Yürütme ayarlarını aç',
'inlineSwitcher.customSuffix': '(özel)',
'project.backToProjects': 'Projelere dön',
'project.metaFreeform': 'serbest stil',
'project.resizeChatPanel': 'Sohbet panelini yeniden boyutlandır',

View file

@ -167,6 +167,19 @@ export const uk: Dict = {
'entry.openSettingsAria': 'Відкрити налаштування',
'entry.resizeAria': 'Змінити розмір бічної панелі',
'entry.loadingWorkspace': 'Завантаження робочого простору…',
'entry.navNewProject': 'Новий проєкт',
'entry.navHome': 'Головна',
'entry.navProjects': 'Проєкти',
'entry.navDesignSystems': 'Дизайн-системи',
'entry.helpAria': 'Довідка',
'entry.helpMenuAria': 'Меню довідки',
'entry.helpGetHelp': 'Отримати допомогу на GitHub',
'entry.helpSubmitFeature': 'Запропонувати функцію',
'entry.helpWhatsNew': 'Що нового',
'entry.helpDownloadDesktop': 'Завантажити настільний застосунок',
'entry.githubStarLabel': 'Star',
'entry.githubStarTitle': 'Поставте нам зірку на GitHub',
'entry.githubStarAria': 'Поставити Open Design зірку на GitHub',
'entry.tabImageTemplates': 'Шаблони зображень',
'entry.tabVideoTemplates': 'Шаблони відеороликів',
'promptTemplates.searchPlaceholder': 'Пошук шаблонів…',
@ -501,6 +514,24 @@ export const uk: Dict = {
'avatar.reasoningLabel': 'Міркування',
'avatar.customSuffix': '(власна)',
'inlineSwitcher.chipTitle': 'Перемкнути CLI / модель',
'inlineSwitcher.chipCli': 'Локальна CLI',
'inlineSwitcher.chipByok': 'BYOK',
'inlineSwitcher.modelDefault': 'за замовчуванням',
'inlineSwitcher.noAgent': 'агент не вибрано',
'inlineSwitcher.modeLabel': 'Режим',
'inlineSwitcher.agentLabel': 'Агент',
'inlineSwitcher.providerLabel': 'Провайдер',
'inlineSwitcher.modelLabel': 'Модель',
'inlineSwitcher.useCli': 'Використовувати локальну CLI',
'inlineSwitcher.useByok': 'Використовувати власний API-ключ',
'inlineSwitcher.daemonOffline': 'Демон офлайн — відкрийте налаштування',
'inlineSwitcher.noAgentsDetected': 'CLI не знайдено в PATH',
'inlineSwitcher.openSettingsForModel': 'Налаштуйте провайдера в Налаштуваннях',
'inlineSwitcher.missingApiKey': 'Не задано API-ключ — відкрийте Налаштування.',
'inlineSwitcher.openFullSettings': 'Відкрити налаштування виконання',
'inlineSwitcher.customSuffix': '(власна)',
'project.backToProjects': 'Назад до проектів',
'project.metaFreeform': 'вільна форма',
'project.resizeChatPanel': 'Змінити розмір панелі чату',

View file

@ -164,6 +164,8 @@ export const zhCN: Dict = {
'entry.openSettingsAria': '打开设置',
'entry.resizeAria': '调整侧边栏宽度',
'entry.loadingWorkspace': '正在加载工作区…',
'entry.useEverywhereTitle': '随处使用',
'entry.useEverywhereAria': '打开「随处使用」指南CLI、MCP、HTTP、Skills',
'entry.navNewProject': '新建项目',
'entry.navHome': '主页',
'entry.navProjects': '项目',

View file

@ -164,6 +164,8 @@ export const zhTW: Dict = {
'entry.openSettingsAria': '開啟設定',
'entry.resizeAria': '調整側邊欄寬度',
'entry.loadingWorkspace': '正在載入工作區…',
'entry.useEverywhereTitle': '隨處使用',
'entry.useEverywhereAria': '開啟「隨處使用」指南CLI、MCP、HTTP、Skills',
'entry.navNewProject': '新建專案',
'entry.navHome': '主頁',
'entry.navProjects': '專案',

View file

@ -320,6 +320,8 @@ export interface Dict {
'entry.openSettingsAria': string;
'entry.resizeAria': string;
'entry.loadingWorkspace': string;
'entry.useEverywhereTitle': string;
'entry.useEverywhereAria': string;
// Left nav rail (icon-only) — surface labels also serve as tooltips
'entry.navNewProject': string;
'entry.navHome': string;

View file

@ -11,3 +11,4 @@
@import './recent-projects.css';
@import './plugins-home.css';
@import './new-project-modal.css';
@import './use-everywhere.css';

View file

@ -0,0 +1,357 @@
/* ============================================================
Use Open Design Everywhere modal that documents Open Design's
non-UI surfaces (CLI, MCP, HTTP, Skills) and lets the user copy
a one-shot guide into their agent (Claude Code, Codex, openclaw,
hermes, etc.).
Block names follow the same BEM convention as `.plugin-details-modal`
so the visual language stays consistent inside the entry shell.
============================================================ */
.use-everywhere-modal-backdrop {
position: fixed;
inset: 0;
z-index: 920;
background: rgba(28, 27, 26, 0.42);
backdrop-filter: blur(2px);
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
}
.use-everywhere-modal {
width: 100%;
max-width: 860px;
max-height: calc(100vh - 64px);
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
display: flex;
flex-direction: column;
overflow: hidden;
}
.use-everywhere-modal__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding: 20px 22px 16px;
border-bottom: 1px solid var(--border);
background: var(--bg-panel);
}
.use-everywhere-modal__head-titles {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.use-everywhere-modal__kicker {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--accent-strong, var(--accent));
font-weight: 600;
}
.use-everywhere-modal__title {
margin: 0;
font-size: 19px;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--text-strong);
}
.use-everywhere-modal__subtitle {
margin: 0;
font-size: 12.5px;
line-height: 1.55;
color: var(--text-muted);
}
.use-everywhere-modal__close {
appearance: none;
background: transparent;
border: 1px solid transparent;
color: var(--text-muted);
width: 28px;
height: 28px;
border-radius: var(--radius-sm, 6px);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;
}
.use-everywhere-modal__close:hover,
.use-everywhere-modal__close:focus-visible {
background: var(--bg-subtle);
color: var(--text);
border-color: var(--border);
}
/* Tab strip — one row of pill buttons for the five surfaces. */
.use-everywhere-modal__tabs {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 10px 22px 0;
border-bottom: 1px dashed var(--border);
background: var(--bg-panel);
}
.use-everywhere-modal__tab {
appearance: none;
border: 1px solid transparent;
background: transparent;
color: var(--text-muted);
font-size: 12px;
font-weight: 500;
padding: 6px 12px 8px;
margin-bottom: -1px;
border-radius: 8px 8px 0 0;
cursor: pointer;
transition: color 120ms ease, background-color 120ms ease,
border-color 120ms ease;
}
.use-everywhere-modal__tab:hover {
color: var(--text);
background: var(--bg-subtle);
}
.use-everywhere-modal__tab.is-active {
color: var(--text-strong);
background: var(--bg);
border-color: var(--border);
border-bottom-color: var(--bg);
}
.use-everywhere-modal__body {
flex: 1;
min-height: 0;
overflow: auto;
padding: 18px 22px 6px;
background: var(--bg);
}
.use-everywhere-section {
display: flex;
flex-direction: column;
gap: 14px;
}
.use-everywhere-section__head {
display: flex;
flex-direction: column;
gap: 6px;
}
.use-everywhere-section__heading {
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--text);
letter-spacing: -0.005em;
}
.use-everywhere-section__intro {
margin: 0;
font-size: 13px;
line-height: 1.6;
color: var(--text-muted);
}
.use-everywhere-section__bullets {
margin: 0;
padding-left: 18px;
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12.5px;
line-height: 1.55;
color: var(--text);
}
.use-everywhere-section__bullets li::marker {
color: var(--text-faint);
}
.use-everywhere-section__snippets {
display: flex;
flex-direction: column;
gap: 12px;
}
.use-everywhere-section__footer {
margin: 0;
font-size: 12px;
line-height: 1.55;
color: var(--text-muted);
background: var(--bg-subtle);
border: 1px dashed var(--border);
border-radius: var(--radius-sm, 6px);
padding: 8px 10px;
}
.use-everywhere-snippet {
border: 1px solid var(--border);
border-radius: var(--radius-sm, 6px);
background: var(--bg-panel);
overflow: hidden;
}
.use-everywhere-snippet__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 8px 10px;
border-bottom: 1px solid var(--border);
background: var(--bg-subtle);
font-size: 11.5px;
color: var(--text-muted);
}
.use-everywhere-snippet__label {
font-weight: 500;
color: var(--text);
}
.use-everywhere-snippet__copy {
appearance: none;
display: inline-flex;
align-items: center;
gap: 4px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--text-muted);
padding: 3px 8px;
border-radius: 999px;
font-size: 11px;
cursor: pointer;
transition: color 120ms ease, border-color 120ms ease;
}
.use-everywhere-snippet__copy:hover {
color: var(--accent);
border-color: var(--accent);
}
.use-everywhere-snippet__pre {
margin: 0;
padding: 12px 14px;
font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
font-size: 12px;
line-height: 1.55;
color: var(--text);
overflow-x: auto;
white-space: pre;
background: var(--bg-panel);
}
.use-everywhere-snippet__pre code {
font: inherit;
color: inherit;
background: transparent;
padding: 0;
}
.use-everywhere-modal__foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 22px;
border-top: 1px solid var(--border);
background: var(--bg-panel);
}
.use-everywhere-modal__foot-info {
font-size: 12px;
line-height: 1.45;
color: var(--text-muted);
flex: 1;
min-width: 0;
}
.use-everywhere-modal__foot-info strong {
color: var(--text-strong);
}
.use-everywhere-modal__foot-actions {
display: inline-flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.use-everywhere-modal__primary,
.use-everywhere-modal__secondary {
appearance: none;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12.5px;
font-weight: 500;
padding: 8px 14px;
border-radius: var(--radius-sm, 6px);
cursor: pointer;
transition: background-color 120ms ease, color 120ms ease,
border-color 120ms ease;
}
.use-everywhere-modal__secondary {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
.use-everywhere-modal__secondary:hover {
background: var(--bg-subtle);
}
.use-everywhere-modal__primary {
background: var(--accent);
color: var(--accent-on, #fff);
border: 1px solid var(--accent);
}
.use-everywhere-modal__primary:hover {
background: var(--accent-strong, var(--accent));
border-color: var(--accent-strong, var(--accent));
}
/* Top-bar entry chip sibling of the existing settings cog avatar.
The chip wraps an icon + label so the affordance reads like an
intentional doc/help entry rather than a generic icon button. On
tight viewports the label collapses and the chip acts as an icon. */
.use-everywhere-chip {
appearance: none;
display: inline-flex;
align-items: center;
gap: 6px;
height: 32px;
padding: 0 12px;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--bg-panel);
color: var(--text);
font-size: 12px;
font-weight: 500;
line-height: 1;
cursor: pointer;
box-shadow: var(--shadow-xs);
transition: background-color 120ms ease, border-color 120ms ease,
color 120ms ease;
}
.use-everywhere-chip:hover {
background: var(--bg-subtle);
border-color: var(--border-strong);
}
.use-everywhere-chip:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.use-everywhere-chip__icon {
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--accent);
flex: 0 0 auto;
}
.use-everywhere-chip__label {
white-space: nowrap;
}
@media (max-width: 700px) {
.use-everywhere-chip__label {
display: none;
}
.use-everywhere-chip {
padding: 0 8px;
}
.use-everywhere-modal__foot {
flex-direction: column;
align-items: stretch;
}
.use-everywhere-modal__foot-actions {
justify-content: flex-end;
}
}