mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
feat(web): add skills & design systems management page in settings (#535)
* feat(web): add skills & design systems management page in settings Add a new "Library" section in Settings that lets users browse, search, preview, and enable/disable skills and design systems. Disabled items are excluded from the create-project picker. Phase 1 — browse/toggle only. Closes #497 * fix(web): persist empty disabled lists and deduplicate DS preview Use empty array instead of undefined when all items are re-enabled so the daemon merge clears the key. Move DS preview panel outside the category group loop so it renders once, not per group. * fix(web): address review feedback on library settings Clear disabled lists on invalid daemon writes, memoize enabled item filters in App.tsx, and guard preview fetch against rapid-click race conditions. * fix(web): hydrate disabled lists from daemon and keep full lists in ProjectView Merge daemonConfig.disabledSkills/disabledDesignSystems during bootstrap so the values survive localStorage resets. Pass unfiltered skills and design systems to ProjectView so existing project metadata resolves correctly.
This commit is contained in:
parent
4fa2df2ae3
commit
cbe2baf596
26 changed files with 952 additions and 3 deletions
|
|
@ -22,6 +22,8 @@ export interface AppConfigPrefs {
|
|||
agentModels?: Record<string, AgentModelPrefs>;
|
||||
skillId?: string | null;
|
||||
designSystemId?: string | null;
|
||||
disabledSkills?: string[];
|
||||
disabledDesignSystems?: string[];
|
||||
}
|
||||
|
||||
const ALLOWED_KEYS: ReadonlySet<keyof AppConfigPrefs> = new Set([
|
||||
|
|
@ -30,6 +32,8 @@ const ALLOWED_KEYS: ReadonlySet<keyof AppConfigPrefs> = new Set([
|
|||
'agentModels',
|
||||
'skillId',
|
||||
'designSystemId',
|
||||
'disabledSkills',
|
||||
'disabledDesignSystems',
|
||||
] as const);
|
||||
|
||||
function configFile(dataDir: string): string {
|
||||
|
|
@ -84,6 +88,13 @@ function applyConfigValue(
|
|||
delete target[key];
|
||||
}
|
||||
}
|
||||
if (key === 'disabledSkills' || key === 'disabledDesignSystems') {
|
||||
if (Array.isArray(value) && value.every((v) => typeof v === 'string')) {
|
||||
target[key] = value;
|
||||
} else {
|
||||
delete target[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function filterAllowedKeys(obj: Record<string, unknown>): AppConfigPrefs {
|
||||
|
|
|
|||
|
|
@ -216,6 +216,49 @@ function httpRequest(
|
|||
});
|
||||
}
|
||||
|
||||
describe('app-config disabled lists', () => {
|
||||
let dataDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
dataDir = await mkdtemp(path.join(tmpdir(), 'od-disabled-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(dataDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('persists disabledSkills as string array', async () => {
|
||||
await writeAppConfig(dataDir, { disabledSkills: ['skill-a', 'skill-b'] });
|
||||
const cfg = await readAppConfig(dataDir);
|
||||
expect(cfg.disabledSkills).toEqual(['skill-a', 'skill-b']);
|
||||
});
|
||||
|
||||
it('persists disabledDesignSystems as string array', async () => {
|
||||
await writeAppConfig(dataDir, { disabledDesignSystems: ['ds-x'] });
|
||||
const cfg = await readAppConfig(dataDir);
|
||||
expect(cfg.disabledDesignSystems).toEqual(['ds-x']);
|
||||
});
|
||||
|
||||
it('drops disabledSkills when not a string array', async () => {
|
||||
await writeAppConfig(dataDir, { disabledSkills: 'not-array' } as any);
|
||||
const cfg = await readAppConfig(dataDir);
|
||||
expect(cfg.disabledSkills).toBeUndefined();
|
||||
});
|
||||
|
||||
it('drops disabledSkills with non-string elements', async () => {
|
||||
await writeAppConfig(dataDir, { disabledSkills: [1, 2, 3] } as any);
|
||||
const cfg = await readAppConfig(dataDir);
|
||||
expect(cfg.disabledSkills).toBeUndefined();
|
||||
});
|
||||
|
||||
it('clears disabledSkills when empty array is sent', async () => {
|
||||
await writeAppConfig(dataDir, { disabledSkills: ['a'] });
|
||||
await writeAppConfig(dataDir, { disabledSkills: [] });
|
||||
const cfg = await readAppConfig(dataDir);
|
||||
expect(cfg.disabledSkills).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('app-config origin guard', () => {
|
||||
let server: http.Server;
|
||||
let port: number;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { EntryView } from './components/EntryView';
|
||||
import type { CreateInput } from './components/NewProjectPanel';
|
||||
import { PetOverlay } from './components/pet/PetOverlay';
|
||||
|
|
@ -182,6 +182,12 @@ export function App() {
|
|||
...daemonConfig.agentModels,
|
||||
};
|
||||
}
|
||||
if (daemonConfig.disabledSkills !== undefined) {
|
||||
next.disabledSkills = daemonConfig.disabledSkills;
|
||||
}
|
||||
if (daemonConfig.disabledDesignSystems !== undefined) {
|
||||
next.disabledDesignSystems = daemonConfig.disabledDesignSystems;
|
||||
}
|
||||
}
|
||||
|
||||
if (alive) {
|
||||
|
|
@ -520,6 +526,18 @@ export function App() {
|
|||
void refreshTemplates();
|
||||
}, [route.kind, refreshTemplates]);
|
||||
|
||||
const enabledSkills = useMemo(
|
||||
() => skills.filter((s) => !(config.disabledSkills ?? []).includes(s.id)),
|
||||
[skills, config.disabledSkills],
|
||||
);
|
||||
const enabledDS = useMemo(
|
||||
() =>
|
||||
designSystems.filter(
|
||||
(d) => !(config.disabledDesignSystems ?? []).includes(d.id),
|
||||
),
|
||||
[designSystems, config.disabledDesignSystems],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{activeProject ? (
|
||||
|
|
@ -548,8 +566,8 @@ export function App() {
|
|||
/>
|
||||
) : (
|
||||
<EntryView
|
||||
skills={skills}
|
||||
designSystems={designSystems}
|
||||
skills={enabledSkills}
|
||||
designSystems={enabledDS}
|
||||
projects={projects}
|
||||
templates={templates}
|
||||
promptTemplates={promptTemplates}
|
||||
|
|
|
|||
369
apps/web/src/components/LibrarySection.tsx
Normal file
369
apps/web/src/components/LibrarySection.tsx
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useT } from '../i18n';
|
||||
import { Icon } from './Icon';
|
||||
import type { AppConfig } from '../types';
|
||||
import type { SkillSummary, DesignSystemSummary } from '@open-design/contracts';
|
||||
import {
|
||||
fetchSkills,
|
||||
fetchDesignSystems,
|
||||
fetchSkill,
|
||||
fetchDesignSystem,
|
||||
} from '../providers/registry';
|
||||
|
||||
type Tab = 'skills' | 'design-systems';
|
||||
|
||||
interface Props {
|
||||
cfg: AppConfig;
|
||||
setCfg: Dispatch<SetStateAction<AppConfig>>;
|
||||
}
|
||||
|
||||
const MODES = [
|
||||
'prototype',
|
||||
'deck',
|
||||
'template',
|
||||
'design-system',
|
||||
'image',
|
||||
'video',
|
||||
'audio',
|
||||
] as const;
|
||||
|
||||
export function LibrarySection({ cfg, setCfg }: Props) {
|
||||
const t = useT();
|
||||
const [tab, setTab] = useState<Tab>('skills');
|
||||
const [search, setSearch] = useState('');
|
||||
const [modeFilter, setModeFilter] = useState('all');
|
||||
const [categoryFilter, setCategoryFilter] = useState('All');
|
||||
const [skills, setSkills] = useState<SkillSummary[]>([]);
|
||||
const [designSystems, setDesignSystems] = useState<DesignSystemSummary[]>([]);
|
||||
const [previewId, setPreviewId] = useState<string | null>(null);
|
||||
const [previewBody, setPreviewBody] = useState<string | null>(null);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSkills().then(setSkills);
|
||||
fetchDesignSystems().then(setDesignSystems);
|
||||
}, []);
|
||||
|
||||
const categories = useMemo(() => {
|
||||
const cats = new Set(designSystems.map((d) => d.category));
|
||||
return ['All', ...Array.from(cats).sort()];
|
||||
}, [designSystems]);
|
||||
|
||||
const disabledSkills = useMemo(
|
||||
() => new Set(cfg.disabledSkills ?? []),
|
||||
[cfg.disabledSkills],
|
||||
);
|
||||
const disabledDS = useMemo(
|
||||
() => new Set(cfg.disabledDesignSystems ?? []),
|
||||
[cfg.disabledDesignSystems],
|
||||
);
|
||||
|
||||
const filteredSkills = useMemo(() => {
|
||||
const q = search.toLowerCase();
|
||||
return skills.filter((s) => {
|
||||
if (modeFilter !== 'all' && s.mode !== modeFilter) return false;
|
||||
if (q && !s.name.toLowerCase().includes(q) && !s.description.toLowerCase().includes(q))
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
}, [skills, modeFilter, search]);
|
||||
|
||||
const filteredDS = useMemo(() => {
|
||||
const q = search.toLowerCase();
|
||||
return designSystems.filter((d) => {
|
||||
if (categoryFilter !== 'All' && d.category !== categoryFilter) return false;
|
||||
if (q && !d.title.toLowerCase().includes(q) && !d.summary.toLowerCase().includes(q))
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
}, [designSystems, categoryFilter, search]);
|
||||
|
||||
const groupedSkills = useMemo(() => {
|
||||
const groups = new Map<string, SkillSummary[]>();
|
||||
for (const s of filteredSkills) {
|
||||
const list = groups.get(s.mode) ?? [];
|
||||
list.push(s);
|
||||
groups.set(s.mode, list);
|
||||
}
|
||||
return groups;
|
||||
}, [filteredSkills]);
|
||||
|
||||
const groupedDS = useMemo(() => {
|
||||
const groups = new Map<string, DesignSystemSummary[]>();
|
||||
for (const d of filteredDS) {
|
||||
const list = groups.get(d.category) ?? [];
|
||||
list.push(d);
|
||||
groups.set(d.category, list);
|
||||
}
|
||||
return groups;
|
||||
}, [filteredDS]);
|
||||
|
||||
const openPreview = useCallback(
|
||||
async (id: string) => {
|
||||
if (previewId === id) {
|
||||
setPreviewId(null);
|
||||
setPreviewBody(null);
|
||||
return;
|
||||
}
|
||||
setPreviewId(id);
|
||||
setPreviewBody(null);
|
||||
setPreviewLoading(true);
|
||||
try {
|
||||
const detail =
|
||||
tab === 'skills'
|
||||
? await fetchSkill(id)
|
||||
: await fetchDesignSystem(id);
|
||||
setPreviewId((cur) => {
|
||||
if (cur === id) setPreviewBody(detail?.body ?? null);
|
||||
return cur;
|
||||
});
|
||||
} catch {
|
||||
setPreviewId((cur) => {
|
||||
if (cur === id) setPreviewBody(null);
|
||||
return cur;
|
||||
});
|
||||
} finally {
|
||||
setPreviewId((cur) => {
|
||||
if (cur === id) setPreviewLoading(false);
|
||||
return cur;
|
||||
});
|
||||
}
|
||||
},
|
||||
[previewId, tab],
|
||||
);
|
||||
|
||||
function toggleSkillDisabled(id: string, disabled: boolean) {
|
||||
setCfg((c) => {
|
||||
const set = new Set(c.disabledSkills ?? []);
|
||||
if (disabled) set.add(id);
|
||||
else set.delete(id);
|
||||
return { ...c, disabledSkills: [...set] };
|
||||
});
|
||||
}
|
||||
|
||||
function toggleDSDisabled(id: string, disabled: boolean) {
|
||||
setCfg((c) => {
|
||||
const set = new Set(c.disabledDesignSystems ?? []);
|
||||
if (disabled) set.add(id);
|
||||
else set.delete(id);
|
||||
return { ...c, disabledDesignSystems: [...set] };
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="settings-section">
|
||||
<div className="section-head">
|
||||
<div>
|
||||
<h3>{t('settings.library')}</h3>
|
||||
<p className="hint">{t('settings.libraryHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="seg-control" role="tablist">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
className={`seg-btn${tab === 'skills' ? ' active' : ''}`}
|
||||
onClick={() => {
|
||||
setTab('skills');
|
||||
setModeFilter('all');
|
||||
setCategoryFilter('All');
|
||||
setSearch('');
|
||||
setPreviewId(null);
|
||||
}}
|
||||
>
|
||||
<span className="seg-title">
|
||||
{t('settings.librarySkills')}
|
||||
<span className="seg-meta">{skills.length}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
className={`seg-btn${tab === 'design-systems' ? ' active' : ''}`}
|
||||
onClick={() => {
|
||||
setTab('design-systems');
|
||||
setModeFilter('all');
|
||||
setCategoryFilter('All');
|
||||
setSearch('');
|
||||
setPreviewId(null);
|
||||
}}
|
||||
>
|
||||
<span className="seg-title">
|
||||
{t('settings.libraryDesignSystems')}
|
||||
<span className="seg-meta">{designSystems.length}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="library-toolbar">
|
||||
<input
|
||||
type="search"
|
||||
className="library-search"
|
||||
placeholder={t('settings.librarySearch')}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
{tab === 'skills' ? (
|
||||
<div className="library-filters">
|
||||
<button
|
||||
type="button"
|
||||
className={`filter-pill${modeFilter === 'all' ? ' active' : ''}`}
|
||||
onClick={() => setModeFilter('all')}
|
||||
>
|
||||
{t('settings.libraryAll')}
|
||||
</button>
|
||||
{MODES.map((mode) => {
|
||||
const count = skills.filter((s) => s.mode === mode).length;
|
||||
if (count === 0) return null;
|
||||
return (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
className={`filter-pill${modeFilter === mode ? ' active' : ''}`}
|
||||
onClick={() => setModeFilter(mode)}
|
||||
>
|
||||
{mode}
|
||||
<span className="filter-pill-count">{count}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="library-filters">
|
||||
{categories.map((cat) => {
|
||||
const count =
|
||||
cat === 'All'
|
||||
? designSystems.length
|
||||
: designSystems.filter((d) => d.category === cat).length;
|
||||
return (
|
||||
<button
|
||||
key={cat}
|
||||
type="button"
|
||||
className={`filter-pill${categoryFilter === cat ? ' active' : ''}`}
|
||||
onClick={() => setCategoryFilter(cat)}
|
||||
>
|
||||
{cat}
|
||||
<span className="filter-pill-count">{count}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="library-content">
|
||||
{tab === 'skills' ? (
|
||||
filteredSkills.length === 0 ? (
|
||||
<p className="library-empty">{t('settings.libraryNoResults')}</p>
|
||||
) : (
|
||||
MODES.filter((m) => groupedSkills.has(m)).map((mode) => (
|
||||
<div key={mode} className="library-group">
|
||||
<h4 className="library-group-title">
|
||||
{mode}{' '}
|
||||
<span className="library-group-count">{groupedSkills.get(mode)!.length}</span>
|
||||
</h4>
|
||||
{groupedSkills.get(mode)!.map((skill) => (
|
||||
<div
|
||||
key={skill.id}
|
||||
className={`library-card${disabledSkills.has(skill.id) ? ' disabled' : ''}`}
|
||||
>
|
||||
<div className="library-card-info">
|
||||
<div className="library-card-title-row">
|
||||
<span className="library-card-name">{skill.name}</span>
|
||||
<span className="library-card-badge">{skill.previewType}</span>
|
||||
</div>
|
||||
<div className="library-card-desc">{skill.description}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="library-card-expand"
|
||||
onClick={() => openPreview(skill.id)}
|
||||
title={t('settings.libraryPreview')}
|
||||
>
|
||||
<Icon
|
||||
name={previewId === skill.id ? 'close' : 'chevron-right'}
|
||||
size={14}
|
||||
/>
|
||||
</button>
|
||||
<label className="toggle-switch" title={t('settings.libraryToggleLabel')}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!disabledSkills.has(skill.id)}
|
||||
onChange={(e) => toggleSkillDisabled(skill.id, !e.target.checked)}
|
||||
/>
|
||||
<span className="toggle-slider" />
|
||||
</label>
|
||||
{previewId === skill.id && (
|
||||
<div className="library-preview">
|
||||
{previewLoading ? (
|
||||
<p>{t('settings.libraryLoading')}</p>
|
||||
) : previewBody ? (
|
||||
<pre className="library-preview-body">{previewBody}</pre>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
)
|
||||
) : filteredDS.length === 0 ? (
|
||||
<p className="library-empty">{t('settings.libraryNoResults')}</p>
|
||||
) : (
|
||||
<>
|
||||
{Array.from(groupedDS.entries()).map(([category, items]) => (
|
||||
<div key={category} className="library-group">
|
||||
<h4 className="library-group-title">
|
||||
{category} <span className="library-group-count">{items.length}</span>
|
||||
</h4>
|
||||
<div className="ds-grid">
|
||||
{items.map((ds) => (
|
||||
<div
|
||||
key={ds.id}
|
||||
className={`library-ds-card${disabledDS.has(ds.id) ? ' disabled' : ''}`}
|
||||
>
|
||||
<div className="library-ds-card-content" onClick={() => openPreview(ds.id)}>
|
||||
{ds.swatches && ds.swatches.length > 0 && (
|
||||
<div className="library-ds-swatches">
|
||||
{ds.swatches.slice(0, 4).map((c, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="library-ds-swatch"
|
||||
style={{ backgroundColor: c }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="library-ds-title">{ds.title}</div>
|
||||
<div className="library-ds-summary">{ds.summary}</div>
|
||||
</div>
|
||||
<label className="toggle-switch toggle-switch-sm" title={t('settings.libraryToggleLabel')}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!disabledDS.has(ds.id)}
|
||||
onChange={(e) => toggleDSDisabled(ds.id, !e.target.checked)}
|
||||
/>
|
||||
<span className="toggle-slider" />
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{previewId && filteredDS.some((d) => d.id === previewId) && (
|
||||
<div className="library-preview">
|
||||
{previewLoading ? (
|
||||
<p>{t('settings.libraryLoading')}</p>
|
||||
) : previewBody ? (
|
||||
<pre className="library-preview-body">{previewBody}</pre>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import type { AgentInfo, ApiProtocol, ApiProtocolConfig, AppConfig, AppTheme, Ap
|
|||
import { MEDIA_PROVIDERS } from '../media/models';
|
||||
import type { MediaProvider } from '../media/models';
|
||||
import { PetSettings } from './pet/PetSettings';
|
||||
import { LibrarySection } from './LibrarySection';
|
||||
import { DEFAULT_NOTIFICATIONS } from '../state/config';
|
||||
import {
|
||||
FAILURE_SOUNDS,
|
||||
|
|
@ -38,6 +39,7 @@ export type SettingsSection =
|
|||
| 'appearance'
|
||||
| 'notifications'
|
||||
| 'pet'
|
||||
| 'library'
|
||||
| 'about';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -507,6 +509,17 @@ export function SettingsDialog({
|
|||
<small>{t('pet.navHint')}</small>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`settings-nav-item${activeSection === 'library' ? ' active' : ''}`}
|
||||
onClick={() => setActiveSection('library')}
|
||||
>
|
||||
<Icon name="grid" size={18} />
|
||||
<span>
|
||||
<strong>{t('settings.library')}</strong>
|
||||
<small>{t('settings.libraryHint')}</small>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`settings-nav-item${activeSection === 'about' ? ' active' : ''}`}
|
||||
|
|
@ -1011,6 +1024,10 @@ export function SettingsDialog({
|
|||
<PetSettings cfg={cfg} setCfg={setCfg} />
|
||||
) : null}
|
||||
|
||||
{activeSection === 'library' ? (
|
||||
<LibrarySection cfg={cfg} setCfg={setCfg} />
|
||||
) : null}
|
||||
|
||||
{activeSection === 'about' ? (
|
||||
<section className="settings-section">
|
||||
<div className="section-head">
|
||||
|
|
|
|||
|
|
@ -875,6 +875,19 @@ export const ar: Dict = {
|
|||
'settings.notifySoundBuzz': 'طنين',
|
||||
'settings.notifySoundTwoToneDown': 'نغمتان هابطتان',
|
||||
'settings.notifySoundThud': 'دمدمة',
|
||||
'settings.library': 'المهارات وأنظمة التصميم',
|
||||
'settings.libraryHint': 'تصفح ومعاينة وتفعيل/تعطيل مكتبة المحتوى الخاصة بك',
|
||||
'settings.librarySkills': 'المهارات',
|
||||
'settings.libraryDesignSystems': 'أنظمة التصميم',
|
||||
'settings.librarySearch': 'بحث...',
|
||||
'settings.libraryAll': 'الكل',
|
||||
'settings.libraryPreview': 'معاينة',
|
||||
'settings.libraryPreviewClose': 'إغلاق',
|
||||
'settings.libraryLoading': 'جارٍ التحميل...',
|
||||
'settings.libraryNoResults': 'لا توجد عناصر تطابق بحثك.',
|
||||
'settings.libraryEnabled': 'مفعّل',
|
||||
'settings.libraryDisabled': 'معطّل',
|
||||
'settings.libraryToggleLabel': 'تبديل',
|
||||
'notify.successTitle': 'اكتملت المهمة',
|
||||
'notify.failureTitle': 'فشلت المهمة',
|
||||
'notify.successBody': 'انتهت جولة.',
|
||||
|
|
|
|||
|
|
@ -829,6 +829,19 @@ export const de: Dict = {
|
|||
'settings.notifySoundBuzz': 'Summen',
|
||||
'settings.notifySoundTwoToneDown': 'Zweiton abwärts',
|
||||
'settings.notifySoundThud': 'Dumpfer Schlag',
|
||||
'settings.library': 'Fähigkeiten & Designsysteme',
|
||||
'settings.libraryHint': 'Inhaltsbibliothek durchsuchen, vorschauen und umschalten',
|
||||
'settings.librarySkills': 'Fähigkeiten',
|
||||
'settings.libraryDesignSystems': 'Designsysteme',
|
||||
'settings.librarySearch': 'Suchen...',
|
||||
'settings.libraryAll': 'Alle',
|
||||
'settings.libraryPreview': 'Vorschau',
|
||||
'settings.libraryPreviewClose': 'Schließen',
|
||||
'settings.libraryLoading': 'Laden...',
|
||||
'settings.libraryNoResults': 'Keine Elemente entsprechen Ihrer Suche.',
|
||||
'settings.libraryEnabled': 'Aktiviert',
|
||||
'settings.libraryDisabled': 'Deaktiviert',
|
||||
'settings.libraryToggleLabel': 'Umschalten',
|
||||
'notify.successTitle': 'Aufgabe abgeschlossen',
|
||||
'notify.failureTitle': 'Aufgabe fehlgeschlagen',
|
||||
'notify.successBody': 'Eine Runde ist abgeschlossen.',
|
||||
|
|
|
|||
|
|
@ -906,6 +906,19 @@ export const en: Dict = {
|
|||
'settings.notifySoundBuzz': 'Buzz',
|
||||
'settings.notifySoundTwoToneDown': 'Two-tone down',
|
||||
'settings.notifySoundThud': 'Thud',
|
||||
'settings.library': 'Skills & Design Systems',
|
||||
'settings.libraryHint': 'Browse, preview, and toggle your content library',
|
||||
'settings.librarySkills': 'Skills',
|
||||
'settings.libraryDesignSystems': 'Design Systems',
|
||||
'settings.librarySearch': 'Search...',
|
||||
'settings.libraryAll': 'All',
|
||||
'settings.libraryPreview': 'Preview',
|
||||
'settings.libraryPreviewClose': 'Close',
|
||||
'settings.libraryLoading': 'Loading...',
|
||||
'settings.libraryNoResults': 'No items match your search.',
|
||||
'settings.libraryEnabled': 'Enabled',
|
||||
'settings.libraryDisabled': 'Disabled',
|
||||
'settings.libraryToggleLabel': 'Toggle',
|
||||
'notify.successTitle': 'Task completed',
|
||||
'notify.failureTitle': 'Task failed',
|
||||
'notify.successBody': 'A turn has finished.',
|
||||
|
|
|
|||
|
|
@ -830,6 +830,19 @@ export const esES: Dict = {
|
|||
'settings.notifySoundBuzz': 'Zumbido',
|
||||
'settings.notifySoundTwoToneDown': 'Dos tonos descendente',
|
||||
'settings.notifySoundThud': 'Golpe',
|
||||
'settings.library': 'Habilidades y sistemas de diseño',
|
||||
'settings.libraryHint': 'Explorar, previsualizar y activar/desactivar tu biblioteca de contenidos',
|
||||
'settings.librarySkills': 'Habilidades',
|
||||
'settings.libraryDesignSystems': 'Sistemas de diseño',
|
||||
'settings.librarySearch': 'Buscar...',
|
||||
'settings.libraryAll': 'Todo',
|
||||
'settings.libraryPreview': 'Vista previa',
|
||||
'settings.libraryPreviewClose': 'Cerrar',
|
||||
'settings.libraryLoading': 'Cargando...',
|
||||
'settings.libraryNoResults': 'Ningún elemento coincide con tu búsqueda.',
|
||||
'settings.libraryEnabled': 'Activado',
|
||||
'settings.libraryDisabled': 'Desactivado',
|
||||
'settings.libraryToggleLabel': 'Alternar',
|
||||
'notify.successTitle': 'Tarea completada',
|
||||
'notify.failureTitle': 'La tarea falló',
|
||||
'notify.successBody': 'Un turno ha terminado.',
|
||||
|
|
|
|||
|
|
@ -907,6 +907,19 @@ export const fa: Dict = {
|
|||
'settings.notifySoundBuzz': 'وزوز',
|
||||
'settings.notifySoundTwoToneDown': 'دو نوای پایینرونده',
|
||||
'settings.notifySoundThud': 'تالاپ',
|
||||
'settings.library': 'مهارتها و سیستمهای طراحی',
|
||||
'settings.libraryHint': 'مرور، پیشنمایش و فعال/غیرفعالسازی کتابخانه محتوای شما',
|
||||
'settings.librarySkills': 'مهارتها',
|
||||
'settings.libraryDesignSystems': 'سیستمهای طراحی',
|
||||
'settings.librarySearch': 'جستجو...',
|
||||
'settings.libraryAll': 'همه',
|
||||
'settings.libraryPreview': 'پیشنمایش',
|
||||
'settings.libraryPreviewClose': 'بستن',
|
||||
'settings.libraryLoading': 'در حال بارگذاری...',
|
||||
'settings.libraryNoResults': 'هیچ موردی با جستجوی شما مطابقت ندارد.',
|
||||
'settings.libraryEnabled': 'فعال',
|
||||
'settings.libraryDisabled': 'غیرفعال',
|
||||
'settings.libraryToggleLabel': 'تغییر وضعیت',
|
||||
'notify.successTitle': 'وظیفه تکمیل شد',
|
||||
'notify.failureTitle': 'وظیفه ناموفق بود',
|
||||
'notify.successBody': 'یک نوبت به پایان رسید.',
|
||||
|
|
|
|||
|
|
@ -875,6 +875,19 @@ export const fr: Dict = {
|
|||
'settings.notifySoundBuzz': 'Buzz',
|
||||
'settings.notifySoundTwoToneDown': 'Bitonale descendante',
|
||||
'settings.notifySoundThud': 'Sourd',
|
||||
'settings.library': 'Compétences et systèmes de design',
|
||||
'settings.libraryHint': 'Parcourir, prévisualiser et activer/désactiver votre bibliothèque de contenus',
|
||||
'settings.librarySkills': 'Compétences',
|
||||
'settings.libraryDesignSystems': 'Systèmes de design',
|
||||
'settings.librarySearch': 'Rechercher...',
|
||||
'settings.libraryAll': 'Tout',
|
||||
'settings.libraryPreview': 'Aperçu',
|
||||
'settings.libraryPreviewClose': 'Fermer',
|
||||
'settings.libraryLoading': 'Chargement...',
|
||||
'settings.libraryNoResults': 'Aucun élément ne correspond à votre recherche.',
|
||||
'settings.libraryEnabled': 'Activé',
|
||||
'settings.libraryDisabled': 'Désactivé',
|
||||
'settings.libraryToggleLabel': 'Basculer',
|
||||
'notify.successTitle': 'Tâche terminée',
|
||||
'notify.failureTitle': 'Tâche échouée',
|
||||
'notify.successBody': 'Un tour est terminé.',
|
||||
|
|
|
|||
|
|
@ -885,6 +885,19 @@ export const hu: Dict = {
|
|||
'settings.notifySoundBuzz': 'Zümmögés',
|
||||
'settings.notifySoundTwoToneDown': 'Kétszólamú ereszkedő',
|
||||
'settings.notifySoundThud': 'Tompa puffanás',
|
||||
'settings.library': 'Készségek és tervezőrendszerek',
|
||||
'settings.libraryHint': 'Tartalomkönyvtár böngészése, előnézete és be-/kikapcsolása',
|
||||
'settings.librarySkills': 'Készségek',
|
||||
'settings.libraryDesignSystems': 'Tervezőrendszerek',
|
||||
'settings.librarySearch': 'Keresés...',
|
||||
'settings.libraryAll': 'Összes',
|
||||
'settings.libraryPreview': 'Előnézet',
|
||||
'settings.libraryPreviewClose': 'Bezárás',
|
||||
'settings.libraryLoading': 'Betöltés...',
|
||||
'settings.libraryNoResults': 'Nincs a keresésnek megfelelő elem.',
|
||||
'settings.libraryEnabled': 'Engedélyezve',
|
||||
'settings.libraryDisabled': 'Letiltva',
|
||||
'settings.libraryToggleLabel': 'Átváltás',
|
||||
'notify.successTitle': 'Feladat befejezve',
|
||||
'notify.failureTitle': 'A feladat meghiúsult',
|
||||
'notify.successBody': 'Egy kör befejeződött.',
|
||||
|
|
|
|||
|
|
@ -828,6 +828,19 @@ export const ja: Dict = {
|
|||
'settings.notifySoundBuzz': 'ブザー',
|
||||
'settings.notifySoundTwoToneDown': '下降2音',
|
||||
'settings.notifySoundThud': 'ドスン',
|
||||
'settings.library': 'スキルとデザインシステム',
|
||||
'settings.libraryHint': 'コンテンツライブラリの閲覧、プレビュー、切り替え',
|
||||
'settings.librarySkills': 'スキル',
|
||||
'settings.libraryDesignSystems': 'デザインシステム',
|
||||
'settings.librarySearch': '検索...',
|
||||
'settings.libraryAll': 'すべて',
|
||||
'settings.libraryPreview': 'プレビュー',
|
||||
'settings.libraryPreviewClose': '閉じる',
|
||||
'settings.libraryLoading': '読み込み中...',
|
||||
'settings.libraryNoResults': '検索条件に一致する項目がありません。',
|
||||
'settings.libraryEnabled': '有効',
|
||||
'settings.libraryDisabled': '無効',
|
||||
'settings.libraryToggleLabel': '切り替え',
|
||||
'notify.successTitle': 'タスクが完了しました',
|
||||
'notify.failureTitle': 'タスクが失敗しました',
|
||||
'notify.successBody': '1ターンが終了しました。',
|
||||
|
|
|
|||
|
|
@ -875,6 +875,19 @@ export const ko: Dict = {
|
|||
'settings.notifySoundBuzz': '버즈',
|
||||
'settings.notifySoundTwoToneDown': '하강 2음',
|
||||
'settings.notifySoundThud': '쿵',
|
||||
'settings.library': '스킬 및 디자인 시스템',
|
||||
'settings.libraryHint': '콘텐츠 라이브러리 찾아보기, 미리보기 및 전환',
|
||||
'settings.librarySkills': '스킬',
|
||||
'settings.libraryDesignSystems': '디자인 시스템',
|
||||
'settings.librarySearch': '검색...',
|
||||
'settings.libraryAll': '전체',
|
||||
'settings.libraryPreview': '미리보기',
|
||||
'settings.libraryPreviewClose': '닫기',
|
||||
'settings.libraryLoading': '불러오는 중...',
|
||||
'settings.libraryNoResults': '검색어와 일치하는 항목이 없습니다.',
|
||||
'settings.libraryEnabled': '활성화됨',
|
||||
'settings.libraryDisabled': '비활성화됨',
|
||||
'settings.libraryToggleLabel': '전환',
|
||||
'notify.successTitle': '작업 완료',
|
||||
'notify.failureTitle': '작업 실패',
|
||||
'notify.successBody': '한 턴이 끝났습니다.',
|
||||
|
|
|
|||
|
|
@ -875,6 +875,19 @@ export const pl: Dict = {
|
|||
'settings.notifySoundBuzz': 'Brzęczenie',
|
||||
'settings.notifySoundTwoToneDown': 'Dwuton malejący',
|
||||
'settings.notifySoundThud': 'Łomot',
|
||||
'settings.library': 'Umiejętności i systemy projektowe',
|
||||
'settings.libraryHint': 'Przeglądaj, podglądaj i włączaj/wyłączaj bibliotekę treści',
|
||||
'settings.librarySkills': 'Umiejętności',
|
||||
'settings.libraryDesignSystems': 'Systemy projektowe',
|
||||
'settings.librarySearch': 'Szukaj...',
|
||||
'settings.libraryAll': 'Wszystkie',
|
||||
'settings.libraryPreview': 'Podgląd',
|
||||
'settings.libraryPreviewClose': 'Zamknij',
|
||||
'settings.libraryLoading': 'Ładowanie...',
|
||||
'settings.libraryNoResults': 'Brak elementów pasujących do wyszukiwania.',
|
||||
'settings.libraryEnabled': 'Włączone',
|
||||
'settings.libraryDisabled': 'Wyłączone',
|
||||
'settings.libraryToggleLabel': 'Przełącz',
|
||||
'notify.successTitle': 'Zadanie ukończone',
|
||||
'notify.failureTitle': 'Zadanie nieudane',
|
||||
'notify.successBody': 'Tura zakończona.',
|
||||
|
|
|
|||
|
|
@ -905,6 +905,19 @@ export const ptBR: Dict = {
|
|||
'settings.notifySoundBuzz': 'Zumbido',
|
||||
'settings.notifySoundTwoToneDown': 'Dois tons descendente',
|
||||
'settings.notifySoundThud': 'Baque',
|
||||
'settings.library': 'Habilidades e sistemas de design',
|
||||
'settings.libraryHint': 'Navegar, visualizar e ativar/desativar sua biblioteca de conteúdos',
|
||||
'settings.librarySkills': 'Habilidades',
|
||||
'settings.libraryDesignSystems': 'Sistemas de design',
|
||||
'settings.librarySearch': 'Pesquisar...',
|
||||
'settings.libraryAll': 'Todos',
|
||||
'settings.libraryPreview': 'Visualizar',
|
||||
'settings.libraryPreviewClose': 'Fechar',
|
||||
'settings.libraryLoading': 'Carregando...',
|
||||
'settings.libraryNoResults': 'Nenhum item corresponde à sua pesquisa.',
|
||||
'settings.libraryEnabled': 'Ativado',
|
||||
'settings.libraryDisabled': 'Desativado',
|
||||
'settings.libraryToggleLabel': 'Alternar',
|
||||
'notify.successTitle': 'Tarefa concluída',
|
||||
'notify.failureTitle': 'Tarefa falhou',
|
||||
'notify.successBody': 'Uma rodada foi concluída.',
|
||||
|
|
|
|||
|
|
@ -905,6 +905,19 @@ export const ru: Dict = {
|
|||
'settings.notifySoundBuzz': 'Жужжание',
|
||||
'settings.notifySoundTwoToneDown': 'Двухтон вниз',
|
||||
'settings.notifySoundThud': 'Глухой удар',
|
||||
'settings.library': 'Навыки и системы дизайна',
|
||||
'settings.libraryHint': 'Просмотр, предпросмотр и управление библиотекой контента',
|
||||
'settings.librarySkills': 'Навыки',
|
||||
'settings.libraryDesignSystems': 'Системы дизайна',
|
||||
'settings.librarySearch': 'Поиск...',
|
||||
'settings.libraryAll': 'Все',
|
||||
'settings.libraryPreview': 'Предпросмотр',
|
||||
'settings.libraryPreviewClose': 'Закрыть',
|
||||
'settings.libraryLoading': 'Загрузка...',
|
||||
'settings.libraryNoResults': 'Ничего не найдено по вашему запросу.',
|
||||
'settings.libraryEnabled': 'Включено',
|
||||
'settings.libraryDisabled': 'Отключено',
|
||||
'settings.libraryToggleLabel': 'Переключить',
|
||||
'notify.successTitle': 'Задача выполнена',
|
||||
'notify.failureTitle': 'Задача завершилась с ошибкой',
|
||||
'notify.successBody': 'Ход завершён.',
|
||||
|
|
|
|||
|
|
@ -874,6 +874,19 @@ export const tr: Dict = {
|
|||
'settings.notifySoundBuzz': 'Vızıltı',
|
||||
'settings.notifySoundTwoToneDown': 'Alçalan iki ton',
|
||||
'settings.notifySoundThud': 'Boğuk vuruş',
|
||||
'settings.library': 'Beceriler ve tasarım sistemleri',
|
||||
'settings.libraryHint': 'İçerik kitaplığınıza göz atın, önizleyin ve açıp kapatın',
|
||||
'settings.librarySkills': 'Beceriler',
|
||||
'settings.libraryDesignSystems': 'Tasarım sistemleri',
|
||||
'settings.librarySearch': 'Ara...',
|
||||
'settings.libraryAll': 'Tümü',
|
||||
'settings.libraryPreview': 'Önizleme',
|
||||
'settings.libraryPreviewClose': 'Kapat',
|
||||
'settings.libraryLoading': 'Yükleniyor...',
|
||||
'settings.libraryNoResults': 'Aramanızla eşleşen öğe bulunamadı.',
|
||||
'settings.libraryEnabled': 'Etkin',
|
||||
'settings.libraryDisabled': 'Devre dışı',
|
||||
'settings.libraryToggleLabel': 'Değiştir',
|
||||
'notify.successTitle': 'Görev tamamlandı',
|
||||
'notify.failureTitle': 'Görev başarısız oldu',
|
||||
'notify.successBody': 'Bir tur tamamlandı.',
|
||||
|
|
|
|||
|
|
@ -906,6 +906,19 @@ export const uk: Dict = {
|
|||
'settings.notifySoundBuzz': 'Гудіння',
|
||||
'settings.notifySoundTwoToneDown': 'Два тони вниз',
|
||||
'settings.notifySoundThud': 'Глухий звук',
|
||||
'settings.library': 'Навички та системи дизайну',
|
||||
'settings.libraryHint': 'Перегляд, попередній перегляд та керування бібліотекою вмісту',
|
||||
'settings.librarySkills': 'Навички',
|
||||
'settings.libraryDesignSystems': 'Системи дизайну',
|
||||
'settings.librarySearch': 'Пошук...',
|
||||
'settings.libraryAll': 'Усі',
|
||||
'settings.libraryPreview': 'Попередній перегляд',
|
||||
'settings.libraryPreviewClose': 'Закрити',
|
||||
'settings.libraryLoading': 'Завантаження...',
|
||||
'settings.libraryNoResults': 'Нічого не знайдено за вашим запитом.',
|
||||
'settings.libraryEnabled': 'Увімкнено',
|
||||
'settings.libraryDisabled': 'Вимкнено',
|
||||
'settings.libraryToggleLabel': 'Перемкнути',
|
||||
'notify.successTitle': 'Завдання завершено',
|
||||
'notify.failureTitle': 'Завдання не вдалося',
|
||||
'notify.successBody': 'Черга завершена.',
|
||||
|
|
|
|||
|
|
@ -887,6 +887,19 @@ export const zhCN: Dict = {
|
|||
'settings.notifySoundBuzz': '蜂鸣',
|
||||
'settings.notifySoundTwoToneDown': '下行双音',
|
||||
'settings.notifySoundThud': '低响',
|
||||
'settings.library': '技能与设计系统',
|
||||
'settings.libraryHint': '浏览、预览和管理您的内容库',
|
||||
'settings.librarySkills': '技能',
|
||||
'settings.libraryDesignSystems': '设计系统',
|
||||
'settings.librarySearch': '搜索...',
|
||||
'settings.libraryAll': '全部',
|
||||
'settings.libraryPreview': '预览',
|
||||
'settings.libraryPreviewClose': '关闭',
|
||||
'settings.libraryLoading': '加载中...',
|
||||
'settings.libraryNoResults': '没有匹配的项目。',
|
||||
'settings.libraryEnabled': '已启用',
|
||||
'settings.libraryDisabled': '已禁用',
|
||||
'settings.libraryToggleLabel': '切换',
|
||||
'notify.successTitle': '任务已完成',
|
||||
'notify.failureTitle': '任务失败',
|
||||
'notify.successBody': '一轮回答已经写完。',
|
||||
|
|
|
|||
|
|
@ -887,6 +887,19 @@ export const zhTW: Dict = {
|
|||
'settings.notifySoundBuzz': '蜂鳴',
|
||||
'settings.notifySoundTwoToneDown': '下行雙音',
|
||||
'settings.notifySoundThud': '低響',
|
||||
'settings.library': '技能與設計系統',
|
||||
'settings.libraryHint': '瀏覽、預覽和管理您的內容庫',
|
||||
'settings.librarySkills': '技能',
|
||||
'settings.libraryDesignSystems': '設計系統',
|
||||
'settings.librarySearch': '搜尋...',
|
||||
'settings.libraryAll': '全部',
|
||||
'settings.libraryPreview': '預覽',
|
||||
'settings.libraryPreviewClose': '關閉',
|
||||
'settings.libraryLoading': '載入中...',
|
||||
'settings.libraryNoResults': '沒有符合的項目。',
|
||||
'settings.libraryEnabled': '已啟用',
|
||||
'settings.libraryDisabled': '已停用',
|
||||
'settings.libraryToggleLabel': '切換',
|
||||
'notify.successTitle': '任務已完成',
|
||||
'notify.failureTitle': '任務失敗',
|
||||
'notify.successBody': '一輪回答已經寫完。',
|
||||
|
|
|
|||
|
|
@ -149,6 +149,19 @@ export interface Dict {
|
|||
'settings.runtimePackaged': string;
|
||||
'settings.runtimeDevelopment': string;
|
||||
'settings.versionUnavailable': string;
|
||||
'settings.library': string;
|
||||
'settings.libraryHint': string;
|
||||
'settings.librarySkills': string;
|
||||
'settings.libraryDesignSystems': string;
|
||||
'settings.librarySearch': string;
|
||||
'settings.libraryAll': string;
|
||||
'settings.libraryPreview': string;
|
||||
'settings.libraryPreviewClose': string;
|
||||
'settings.libraryLoading': string;
|
||||
'settings.libraryNoResults': string;
|
||||
'settings.libraryEnabled': string;
|
||||
'settings.libraryDisabled': string;
|
||||
'settings.libraryToggleLabel': string;
|
||||
|
||||
// Notifications (settings + system notifications)
|
||||
'settings.notifications': string;
|
||||
|
|
|
|||
|
|
@ -10185,3 +10185,266 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
|
|||
[dir="rtl"] .meta {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Library section (Skills & Design Systems management) */
|
||||
|
||||
.library-toolbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.library-search {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.library-search:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.library-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.library-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.library-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.library-group-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.library-group-count {
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.library-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.library-card.disabled {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.library-card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.library-card-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.library-card-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.library-card-badge {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--accent-tint);
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.library-card-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.library-card-expand {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
color: var(--text-muted);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.library-card-expand:hover {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.library-ds-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
padding: 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.library-ds-card.disabled {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.library-ds-card-content {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.library-ds-swatches {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.library-ds-swatch {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
.library-ds-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.library-ds-summary {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.library-ds-card .toggle-switch {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.library-preview {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.library-preview-body {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.library-empty {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
/* Toggle switch */
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--border);
|
||||
border-radius: 20px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.toggle-slider::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider {
|
||||
background-color: var(--accent);
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider::before {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
.toggle-switch-sm {
|
||||
width: 30px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
.toggle-switch-sm .toggle-slider::before {
|
||||
height: 11px;
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
.toggle-switch-sm input:checked + .toggle-slider::before {
|
||||
transform: translateX(13px);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -353,6 +353,8 @@ export async function syncConfigToDaemon(config: AppConfig): Promise<void> {
|
|||
agentModels: config.agentModels,
|
||||
skillId: config.skillId,
|
||||
designSystemId: config.designSystemId,
|
||||
disabledSkills: config.disabledSkills,
|
||||
disabledDesignSystems: config.disabledDesignSystems,
|
||||
};
|
||||
try {
|
||||
await fetch('/api/app-config', {
|
||||
|
|
|
|||
|
|
@ -269,6 +269,9 @@ export interface AppConfig {
|
|||
// configs that pre-date the feature land at `undefined`, which the loader
|
||||
// normalizes to a safe default (everything off).
|
||||
notifications?: NotificationsConfig;
|
||||
// IDs of skills/design-systems the user has explicitly disabled.
|
||||
disabledSkills?: string[];
|
||||
disabledDesignSystems?: string[];
|
||||
}
|
||||
|
||||
export interface ComposioSettings {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ export interface AppConfigPrefs {
|
|||
agentModels?: Record<string, AgentModelPrefs>;
|
||||
skillId?: string | null;
|
||||
designSystemId?: string | null;
|
||||
disabledSkills?: string[];
|
||||
disabledDesignSystems?: string[];
|
||||
}
|
||||
|
||||
export interface AppConfigResponse {
|
||||
|
|
|
|||
Loading…
Reference in a new issue