mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
feat(web): add brand design systems, card thumbnails, and DESIGN.md side-by-side preview (#289)
* feat(web): add brand design systems, card thumbnails, and DESIGN.md side-by-side preview - Add 7 new brand design systems (arc, canva, discord, duolingo, github, huggingface, openai) - Show live showcase HTML thumbnails on Design Systems cards - Add toggleable DESIGN.md side panel in preview modal with syntax-highlighted spec view - Make preview iframe responsive: render at fixed design viewport and scale to fit so opening the side panel never reflows showcases into broken breakpoints - Add floating collapse/expand handles on the sidebar boundary for direct hide/show Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): guard ResizeObserver and re-fire sidebar lazy-load on content swap - Guard `new ResizeObserver(...)` in PreviewModal so the modal mounts in jsdom (the existing preview-modal-fullscreen test was failing with `ReferenceError: ResizeObserver is not defined`) and in older embedded WebViews. Fall back to a window resize listener when the constructor is unavailable. - Add a `contentKey` hint to PreviewSidebar so the lazy-load `onToggle` callback re-fires when the underlying side-panel source swaps while the sidebar stays open. Wire `system.id` through from DesignSystemPreviewModal so swapping design systems with the spec panel open primes a fresh DESIGN.md fetch instead of leaving it stuck. Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code) * fix(web/i18n): add missing ds/preview keys to hu locale The Validate workspace check failed after main's hu.ts landed without the four i18n keys introduced by this PR (ds.specToggle, ds.specLoading, preview.showSidebar, preview.hideSidebar). Generated-By: looper 0.4.0 (runner=fixer, agent=claude-code) --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
bf44394f91
commit
9ee2c1994c
27 changed files with 1883 additions and 75 deletions
94
apps/web/src/components/DesignSpecView.tsx
Normal file
94
apps/web/src/components/DesignSpecView.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
source: string | null | undefined;
|
||||||
|
loading?: boolean;
|
||||||
|
loadingLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render a DESIGN.md as a lightly syntax-coloured monospace source view —
|
||||||
|
// the right-hand panel of the preview modal, mirroring the layout used by
|
||||||
|
// styles.refero.design where the rendered showcase sits next to the spec
|
||||||
|
// text. Highlights are CSS-class only; no innerHTML for untrusted text.
|
||||||
|
export function DesignSpecView({ source, loading, loadingLabel }: Props) {
|
||||||
|
const lines = useMemo(() => (source ? source.split(/\r?\n/) : []), [source]);
|
||||||
|
|
||||||
|
if (loading || source === undefined || source === null) {
|
||||||
|
return <div className="design-spec-empty">{loadingLabel}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre className="design-spec-pre">
|
||||||
|
<code>
|
||||||
|
{lines.map((line, idx) => (
|
||||||
|
<span key={idx} className={`design-spec-line ${classifyLine(line)}`}>
|
||||||
|
{renderInline(line)}
|
||||||
|
{'\n'}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function classifyLine(line: string): string {
|
||||||
|
if (/^#{1,6}\s+/.test(line)) {
|
||||||
|
const hashes = /^(#+)\s/.exec(line)?.[1]?.length ?? 1;
|
||||||
|
return `is-h${Math.min(hashes, 4)}`;
|
||||||
|
}
|
||||||
|
if (/^>\s/.test(line)) return 'is-quote';
|
||||||
|
if (/^[-*+]\s/.test(line.trimStart())) return 'is-list';
|
||||||
|
if (/^\|.*\|\s*$/.test(line)) return 'is-table';
|
||||||
|
if (/^\s*```/.test(line)) return 'is-fence';
|
||||||
|
if (/^\s*$/.test(line)) return 'is-blank';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOKEN_RE = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`|#[0-9a-fA-F]{3,8}\b)/g;
|
||||||
|
|
||||||
|
function renderInline(line: string) {
|
||||||
|
if (!line) return null;
|
||||||
|
const out: (string | JSX.Element)[] = [];
|
||||||
|
let last = 0;
|
||||||
|
let key = 0;
|
||||||
|
for (const match of line.matchAll(TOKEN_RE)) {
|
||||||
|
const start = match.index ?? 0;
|
||||||
|
if (start > last) out.push(line.slice(last, start));
|
||||||
|
const token = match[0];
|
||||||
|
if (token.startsWith('**')) {
|
||||||
|
out.push(
|
||||||
|
<span key={key++} className="md-tk-bold">
|
||||||
|
{token.slice(2, -2)}
|
||||||
|
</span>,
|
||||||
|
);
|
||||||
|
} else if (token.startsWith('*')) {
|
||||||
|
out.push(
|
||||||
|
<span key={key++} className="md-tk-em">
|
||||||
|
{token.slice(1, -1)}
|
||||||
|
</span>,
|
||||||
|
);
|
||||||
|
} else if (token.startsWith('`')) {
|
||||||
|
out.push(
|
||||||
|
<span key={key++} className="md-tk-code">
|
||||||
|
{token.slice(1, -1)}
|
||||||
|
</span>,
|
||||||
|
);
|
||||||
|
} else if (token.startsWith('#')) {
|
||||||
|
out.push(
|
||||||
|
<span key={key++} className="md-tk-color" style={{ color: 'inherit' }}>
|
||||||
|
<span
|
||||||
|
className="md-tk-color-swatch"
|
||||||
|
style={{ backgroundColor: token }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
{token}
|
||||||
|
</span>,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
out.push(token);
|
||||||
|
}
|
||||||
|
last = start + token.length;
|
||||||
|
}
|
||||||
|
if (last < line.length) out.push(line.slice(last));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useT } from '../i18n';
|
import { useT } from '../i18n';
|
||||||
import {
|
import {
|
||||||
|
fetchDesignSystem,
|
||||||
fetchDesignSystemPreview,
|
fetchDesignSystemPreview,
|
||||||
fetchDesignSystemShowcase,
|
fetchDesignSystemShowcase,
|
||||||
} from '../providers/registry';
|
} from '../providers/registry';
|
||||||
import type { DesignSystemSummary } from '../types';
|
import type { DesignSystemSummary } from '../types';
|
||||||
|
import { DesignSpecView } from './DesignSpecView';
|
||||||
import { PreviewModal } from './PreviewModal';
|
import { PreviewModal } from './PreviewModal';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -14,11 +16,14 @@ interface Props {
|
||||||
|
|
||||||
// Two-tab DS preview: a complete Showcase webpage rendered from the system's
|
// Two-tab DS preview: a complete Showcase webpage rendered from the system's
|
||||||
// tokens, and the original Tokens view (palette / typography / components +
|
// tokens, and the original Tokens view (palette / typography / components +
|
||||||
// rendered DESIGN.md prose).
|
// rendered DESIGN.md prose). A toggleable side panel surfaces the raw
|
||||||
|
// DESIGN.md so users can compare spec to render at the same time, mirroring
|
||||||
|
// the styles.refero.design layout.
|
||||||
export function DesignSystemPreviewModal({ system, onClose }: Props) {
|
export function DesignSystemPreviewModal({ system, onClose }: Props) {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const [showcaseHtml, setShowcaseHtml] = useState<string | null | undefined>(undefined);
|
const [showcaseHtml, setShowcaseHtml] = useState<string | null | undefined>(undefined);
|
||||||
const [tokensHtml, setTokensHtml] = useState<string | null | undefined>(undefined);
|
const [tokensHtml, setTokensHtml] = useState<string | null | undefined>(undefined);
|
||||||
|
const [specBody, setSpecBody] = useState<string | null | undefined>(undefined);
|
||||||
|
|
||||||
// Lazy-load each view on first reveal. Both endpoints are cheap, but this
|
// Lazy-load each view on first reveal. Both endpoints are cheap, but this
|
||||||
// keeps the network panel quiet when the user only opens one tab.
|
// keeps the network panel quiet when the user only opens one tab.
|
||||||
|
|
@ -36,10 +41,24 @@ export function DesignSystemPreviewModal({ system, onClose }: Props) {
|
||||||
[system.id, showcaseHtml, tokensHtml],
|
[system.id, showcaseHtml, tokensHtml],
|
||||||
);
|
);
|
||||||
|
|
||||||
// If the system swaps under us (rare but possible), wipe both caches.
|
// Fetch DESIGN.md the first time the side panel opens. Once we have it we
|
||||||
|
// never re-fetch unless the underlying system swaps.
|
||||||
|
const handleSidebarToggle = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
if (!open || specBody !== undefined) return;
|
||||||
|
setSpecBody(null);
|
||||||
|
void fetchDesignSystem(system.id).then((detail) =>
|
||||||
|
setSpecBody(detail?.body ?? null),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[system.id, specBody],
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the system swaps under us (rare but possible), wipe all caches.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowcaseHtml(undefined);
|
setShowcaseHtml(undefined);
|
||||||
setTokensHtml(undefined);
|
setTokensHtml(undefined);
|
||||||
|
setSpecBody(undefined);
|
||||||
}, [system.id]);
|
}, [system.id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -54,6 +73,20 @@ export function DesignSystemPreviewModal({ system, onClose }: Props) {
|
||||||
onView={handleView}
|
onView={handleView}
|
||||||
exportTitleFor={(viewId) => `${system.title} — ${viewId}`}
|
exportTitleFor={(viewId) => `${system.title} — ${viewId}`}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
sidebar={{
|
||||||
|
label: t('ds.specToggle'),
|
||||||
|
defaultOpen: true,
|
||||||
|
onToggle: handleSidebarToggle,
|
||||||
|
// Re-fire onToggle when the system swaps under us so the new
|
||||||
|
// DESIGN.md fetch starts even if the sidebar never closed.
|
||||||
|
contentKey: system.id,
|
||||||
|
content: (
|
||||||
|
<DesignSpecView
|
||||||
|
source={specBody}
|
||||||
|
loadingLabel={t('ds.specLoading')}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useI18n } from '../i18n';
|
import { useI18n } from '../i18n';
|
||||||
import {
|
import {
|
||||||
localizeDesignSystemCategory,
|
localizeDesignSystemCategory,
|
||||||
localizeDesignSystemSummary,
|
localizeDesignSystemSummary,
|
||||||
} from '../i18n/content';
|
} from '../i18n/content';
|
||||||
|
import { fetchDesignSystemShowcase } from '../providers/registry';
|
||||||
|
import { buildSrcdoc } from '../runtime/srcdoc';
|
||||||
import type { DesignSystemSummary, Surface } from '../types';
|
import type { DesignSystemSummary, Surface } from '../types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -45,6 +47,10 @@ export function DesignSystemsTab({ systems, selectedId, onSelect, onPreview }: P
|
||||||
const [filter, setFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
const [surfaceFilter, setSurfaceFilter] = useState<SurfaceFilter>('all');
|
const [surfaceFilter, setSurfaceFilter] = useState<SurfaceFilter>('all');
|
||||||
const [category, setCategory] = useState<string>('All');
|
const [category, setCategory] = useState<string>('All');
|
||||||
|
// Cache fetched showcase HTML across re-renders so cards never re-flicker
|
||||||
|
// when the user filters / scrolls back. null = "in flight"; undefined =
|
||||||
|
// "not yet requested". Mirrors the pattern used by ExamplesTab.
|
||||||
|
const [thumbs, setThumbs] = useState<Record<string, string | null>>({});
|
||||||
|
|
||||||
const surfaceScoped = useMemo(
|
const surfaceScoped = useMemo(
|
||||||
() => surfaceFilter === 'all' ? systems : systems.filter((s) => surfaceOf(s) === surfaceFilter),
|
() => surfaceFilter === 'all' ? systems : systems.filter((s) => surfaceOf(s) === surfaceFilter),
|
||||||
|
|
@ -93,6 +99,16 @@ export function DesignSystemsTab({ systems, selectedId, onSelect, onPreview }: P
|
||||||
return localizeDesignSystemCategory(locale, c);
|
return localizeDesignSystemCategory(locale, c);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function loadThumb(id: string) {
|
||||||
|
setThumbs((prev) => {
|
||||||
|
if (prev[id] !== undefined) return prev;
|
||||||
|
void fetchDesignSystemShowcase(id).then((html) => {
|
||||||
|
setThumbs((p) => ({ ...p, [id]: html }));
|
||||||
|
});
|
||||||
|
return { ...prev, [id]: null };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tab-panel">
|
<div className="tab-panel">
|
||||||
<div className="tab-panel-toolbar">
|
<div className="tab-panel-toolbar">
|
||||||
|
|
@ -135,55 +151,154 @@ export function DesignSystemsTab({ systems, selectedId, onSelect, onPreview }: P
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<div className="tab-empty">{t('ds.emptyNoMatch')}</div>
|
<div className="tab-empty">{t('ds.emptyNoMatch')}</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="ds-list">
|
<div className="ds-grid">
|
||||||
{filtered.map((s) => {
|
{filtered.map((s) => (
|
||||||
const active = s.id === selectedId;
|
<DesignSystemCard
|
||||||
return (
|
key={s.id}
|
||||||
<div
|
system={s}
|
||||||
key={s.id}
|
active={s.id === selectedId}
|
||||||
className={`ds-row ${active ? 'active' : ''}`}
|
thumbHtml={thumbs[s.id]}
|
||||||
onClick={() => onSelect(s.id)}
|
onIntersect={() => loadThumb(s.id)}
|
||||||
>
|
onSelect={() => onSelect(s.id)}
|
||||||
<div className="ds-row-body">
|
onPreview={() => onPreview(s.id)}
|
||||||
<div className="ds-row-title">
|
/>
|
||||||
{s.title}
|
))}
|
||||||
{active ? (
|
|
||||||
<span className="ds-row-default">
|
|
||||||
{t('ds.badgeDefault')}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="ds-row-summary">
|
|
||||||
{localizeDesignSystemSummary(locale, s)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{s.swatches && s.swatches.length > 0 ? (
|
|
||||||
<div className="ds-row-swatches" aria-hidden>
|
|
||||||
{s.swatches.map((c, i) => (
|
|
||||||
<span
|
|
||||||
key={i}
|
|
||||||
className="ds-row-swatch"
|
|
||||||
style={{ background: c }}
|
|
||||||
title={c}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<button
|
|
||||||
className="ghost"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onPreview(s.id);
|
|
||||||
}}
|
|
||||||
title={t('ds.previewTitle')}
|
|
||||||
>
|
|
||||||
{t('ds.preview')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CardProps {
|
||||||
|
system: DesignSystemSummary;
|
||||||
|
active: boolean;
|
||||||
|
thumbHtml: string | null | undefined;
|
||||||
|
onIntersect: () => void;
|
||||||
|
onSelect: () => void;
|
||||||
|
onPreview: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DesignSystemCard({
|
||||||
|
system,
|
||||||
|
active,
|
||||||
|
thumbHtml,
|
||||||
|
onIntersect,
|
||||||
|
onSelect,
|
||||||
|
onPreview,
|
||||||
|
}: CardProps) {
|
||||||
|
const { locale, t } = useI18n();
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
// Lazy-load the showcase iframe only when the card scrolls into the
|
||||||
|
// viewport. With ~120 design systems we can't afford to mount every
|
||||||
|
// iframe up front — even with `loading="lazy"`, srcDoc iframes ignore
|
||||||
|
// the native lazy hint, so we gate via IntersectionObserver.
|
||||||
|
useEffect(() => {
|
||||||
|
if (thumbHtml !== undefined) return;
|
||||||
|
const node = ref.current;
|
||||||
|
if (!node || typeof IntersectionObserver === 'undefined') {
|
||||||
|
onIntersect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
onIntersect();
|
||||||
|
observer.disconnect();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ rootMargin: '200px' },
|
||||||
|
);
|
||||||
|
observer.observe(node);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [thumbHtml, onIntersect]);
|
||||||
|
|
||||||
|
const localizedSummary = localizeDesignSystemSummary(locale, system);
|
||||||
|
const categoryLabel = localizeDesignSystemCategory(
|
||||||
|
locale,
|
||||||
|
system.category || 'Uncategorized',
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={`ds-card ${active ? 'active' : ''}`}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={onSelect}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
onSelect();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="ds-card-thumb"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onPreview();
|
||||||
|
}}
|
||||||
|
title={t('ds.previewTitle')}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onPreview();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{thumbHtml ? (
|
||||||
|
<iframe
|
||||||
|
title={`${system.title} preview`}
|
||||||
|
sandbox="allow-scripts"
|
||||||
|
srcDoc={buildSrcdoc(thumbHtml)}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="ds-card-thumb-fallback" aria-hidden>
|
||||||
|
{system.swatches && system.swatches.length > 0 ? (
|
||||||
|
<div className="ds-card-thumb-swatches">
|
||||||
|
{system.swatches.map((c, i) => (
|
||||||
|
<span key={i} style={{ background: c }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="ds-card-thumb-placeholder">
|
||||||
|
{thumbHtml === null ? '' : ''}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="ds-card-thumb-overlay" aria-hidden>
|
||||||
|
{t('ds.preview')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="ds-card-meta">
|
||||||
|
<div className="ds-card-title-row">
|
||||||
|
<span className="ds-card-title">{system.title}</span>
|
||||||
|
{active ? (
|
||||||
|
<span className="ds-card-badge">{t('ds.badgeDefault')}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="ds-card-summary">{localizedSummary}</div>
|
||||||
|
<div className="ds-card-footer">
|
||||||
|
<span className="ds-card-category">{categoryLabel}</span>
|
||||||
|
{system.swatches && system.swatches.length > 0 ? (
|
||||||
|
<div className="ds-card-swatches" aria-hidden>
|
||||||
|
{system.swatches.map((c, i) => (
|
||||||
|
<span key={i} style={{ background: c }} title={c} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||||
import { useT } from '../i18n';
|
import { useT } from '../i18n';
|
||||||
import { exportAsHtml, exportAsPdf, exportAsZip } from '../runtime/exports';
|
import { exportAsHtml, exportAsPdf, exportAsZip } from '../runtime/exports';
|
||||||
import { buildSrcdoc } from '../runtime/srcdoc';
|
import { buildSrcdoc } from '../runtime/srcdoc';
|
||||||
|
|
@ -12,6 +12,25 @@ export interface PreviewView {
|
||||||
html: string | null | undefined;
|
html: string | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PreviewSidebar {
|
||||||
|
// Header label and toggle button label.
|
||||||
|
label: string;
|
||||||
|
// Side-pane content — caller renders whatever it likes (markdown source
|
||||||
|
// view, swatch grid, etc.). Always optional; when absent the toggle is
|
||||||
|
// not shown.
|
||||||
|
content: ReactNode;
|
||||||
|
// Default open state on first mount. Defaults to false.
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
// Called whenever the open state changes — useful so the parent can
|
||||||
|
// lazy-fetch the side content the first time it is revealed.
|
||||||
|
onToggle?: (open: boolean) => void;
|
||||||
|
// Stable identity for the side-panel source. When this changes while the
|
||||||
|
// sidebar is open, the lazy-load `onToggle` callback re-fires so the parent
|
||||||
|
// can prime a fresh fetch — e.g. swapping between design systems while the
|
||||||
|
// DESIGN.md panel stays open.
|
||||||
|
contentKey?: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
|
|
@ -25,6 +44,16 @@ interface Props {
|
||||||
// a loader callback in.
|
// a loader callback in.
|
||||||
onView?: (viewId: string) => void;
|
onView?: (viewId: string) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
// Optional split-view companion pane shown to the right of the iframe.
|
||||||
|
// Used by the design-system preview to surface the raw DESIGN.md beside
|
||||||
|
// the rendered showcase, matching the styles.refero.design layout.
|
||||||
|
sidebar?: PreviewSidebar;
|
||||||
|
// Logical viewport width the iframe content is rendered at. The iframe is
|
||||||
|
// then visually scaled (transform: scale) to fit the actual stage width
|
||||||
|
// so squeezing the preview behind a sidebar never reflows the inner page
|
||||||
|
// into a half-broken responsive breakpoint. Defaults to 1280 — wide
|
||||||
|
// enough that desktop-shaped showcases keep their intended layout.
|
||||||
|
designWidth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A full-screen overlay that renders an iframe of arbitrary HTML, with an
|
// A full-screen overlay that renders an iframe of arbitrary HTML, with an
|
||||||
|
|
@ -39,6 +68,8 @@ export function PreviewModal({
|
||||||
exportTitleFor,
|
exportTitleFor,
|
||||||
onView,
|
onView,
|
||||||
onClose,
|
onClose,
|
||||||
|
sidebar,
|
||||||
|
designWidth = 1280,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const initial = initialViewId && views.some((v) => v.id === initialViewId)
|
const initial = initialViewId && views.some((v) => v.id === initialViewId)
|
||||||
|
|
@ -47,8 +78,32 @@ export function PreviewModal({
|
||||||
const [activeId, setActiveId] = useState<string>(initial);
|
const [activeId, setActiveId] = useState<string>(initial);
|
||||||
const [shareOpen, setShareOpen] = useState(false);
|
const [shareOpen, setShareOpen] = useState(false);
|
||||||
const [fullscreen, setFullscreen] = useState(false);
|
const [fullscreen, setFullscreen] = useState(false);
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState<boolean>(
|
||||||
|
sidebar?.defaultOpen ?? false,
|
||||||
|
);
|
||||||
const shareRef = useRef<HTMLDivElement | null>(null);
|
const shareRef = useRef<HTMLDivElement | null>(null);
|
||||||
const stageRef = useRef<HTMLDivElement | null>(null);
|
const stageRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const stageFrameRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [stageSize, setStageSize] = useState<{ w: number; h: number }>({
|
||||||
|
w: 0,
|
||||||
|
h: 0,
|
||||||
|
});
|
||||||
|
// Capture the toggle handler in a ref so the lazy-load effect below
|
||||||
|
// depends only on sidebarOpen — without this, a new `sidebar` object on
|
||||||
|
// every parent render would re-fire the load on each render.
|
||||||
|
const sidebarToggleRef = useRef(sidebar?.onToggle);
|
||||||
|
sidebarToggleRef.current = sidebar?.onToggle;
|
||||||
|
|
||||||
|
// Tell the parent every time the side pane toggles so it can lazy-load
|
||||||
|
// the spec body the first time it is revealed. Also re-fires when
|
||||||
|
// `sidebar.contentKey` changes so the parent can prime a fresh fetch when
|
||||||
|
// its underlying source swaps (e.g. another design system) while the
|
||||||
|
// sidebar stays open. `sidebar` itself is a fresh object on every parent
|
||||||
|
// render so we can't depend on it.
|
||||||
|
const sidebarContentKey = sidebar?.contentKey;
|
||||||
|
useEffect(() => {
|
||||||
|
sidebarToggleRef.current?.(sidebarOpen);
|
||||||
|
}, [sidebarOpen, sidebarContentKey]);
|
||||||
|
|
||||||
// Tell the parent the initial view id so it can prime a fetch. Re-fires on
|
// Tell the parent the initial view id so it can prime a fetch. Re-fires on
|
||||||
// tab change. Guarded against re-firing while the same id is active to
|
// tab change. Guarded against re-firing while the same id is active to
|
||||||
|
|
@ -115,6 +170,31 @@ export function PreviewModal({
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Track the iframe stage size so we can render the document at a fixed
|
||||||
|
// logical width and visually scale it down to fit. Without this, opening
|
||||||
|
// the side panel squeezes the iframe to ~60% width and triggers awkward
|
||||||
|
// mid-breakpoint reflows in the showcase HTML.
|
||||||
|
// ResizeObserver is missing from jsdom and from some older embedded
|
||||||
|
// WebViews — guard the constructor and fall back to a window resize
|
||||||
|
// listener so the modal still mounts and just loses element-level
|
||||||
|
// resize tracking.
|
||||||
|
useEffect(() => {
|
||||||
|
const el = stageFrameRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const measure = () => {
|
||||||
|
const r = el.getBoundingClientRect();
|
||||||
|
setStageSize({ w: r.width, h: r.height });
|
||||||
|
};
|
||||||
|
measure();
|
||||||
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
|
const ro = new ResizeObserver(measure);
|
||||||
|
ro.observe(el);
|
||||||
|
return () => ro.disconnect();
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', measure);
|
||||||
|
return () => window.removeEventListener('resize', measure);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const activeView = views.find((v) => v.id === activeId) ?? views[0];
|
const activeView = views.find((v) => v.id === activeId) ?? views[0];
|
||||||
const activeHtml = activeView?.html ?? null;
|
const activeHtml = activeView?.html ?? null;
|
||||||
const srcDoc = useMemo(
|
const srcDoc = useMemo(
|
||||||
|
|
@ -123,6 +203,24 @@ export function PreviewModal({
|
||||||
);
|
);
|
||||||
const exportTitle = exportTitleFor(activeView?.id ?? '');
|
const exportTitle = exportTitleFor(activeView?.id ?? '');
|
||||||
|
|
||||||
|
// Only down-scale: when the stage is wider than the design viewport we
|
||||||
|
// render the iframe at native size instead of upscaling pixels.
|
||||||
|
const scale = stageSize.w > 0 ? Math.min(1, stageSize.w / designWidth) : 1;
|
||||||
|
const scalerStyle = useMemo(() => {
|
||||||
|
if (scale >= 1 || stageSize.w === 0) {
|
||||||
|
return {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
transform: 'none',
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: designWidth,
|
||||||
|
height: stageSize.h / scale,
|
||||||
|
transform: `scale(${scale})`,
|
||||||
|
} as const;
|
||||||
|
}, [scale, stageSize.w, stageSize.h, designWidth]);
|
||||||
|
|
||||||
function openInNewTab() {
|
function openInNewTab() {
|
||||||
if (!activeHtml) return;
|
if (!activeHtml) return;
|
||||||
const blob = new Blob([activeHtml], { type: 'text/html' });
|
const blob = new Blob([activeHtml], { type: 'text/html' });
|
||||||
|
|
@ -177,6 +275,16 @@ export function PreviewModal({
|
||||||
<span aria-hidden="true" />
|
<span aria-hidden="true" />
|
||||||
)}
|
)}
|
||||||
<div className="ds-modal-actions">
|
<div className="ds-modal-actions">
|
||||||
|
{sidebar ? (
|
||||||
|
<button
|
||||||
|
className={`ghost ${sidebarOpen ? 'is-active' : ''}`}
|
||||||
|
onClick={() => setSidebarOpen((v) => !v)}
|
||||||
|
aria-pressed={sidebarOpen}
|
||||||
|
title={sidebar.label}
|
||||||
|
>
|
||||||
|
{sidebar.label}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
<button
|
<button
|
||||||
className="ghost"
|
className="ghost"
|
||||||
onClick={fullscreen ? exitFullscreen : enterFullscreen}
|
onClick={fullscreen ? exitFullscreen : enterFullscreen}
|
||||||
|
|
@ -263,22 +371,54 @@ export function PreviewModal({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="ds-modal-stage" ref={stageRef}>
|
<div
|
||||||
{activeHtml === null || activeHtml === undefined ? (
|
className={`ds-modal-stage ${sidebar && sidebarOpen ? 'has-sidebar' : ''}`}
|
||||||
<div className="ds-modal-empty">
|
ref={stageRef}
|
||||||
{t('preview.loading', {
|
>
|
||||||
label:
|
<div className="ds-modal-stage-iframe" ref={stageFrameRef}>
|
||||||
activeView?.label.toLowerCase() ?? t('common.preview').toLowerCase(),
|
{activeHtml === null || activeHtml === undefined ? (
|
||||||
})}
|
<div className="ds-modal-empty">
|
||||||
</div>
|
{t('preview.loading', {
|
||||||
) : (
|
label:
|
||||||
<iframe
|
activeView?.label.toLowerCase() ?? t('common.preview').toLowerCase(),
|
||||||
key={activeView?.id ?? 'view'}
|
})}
|
||||||
title={`${title} ${activeView?.label ?? ''}`}
|
</div>
|
||||||
sandbox="allow-scripts allow-same-origin"
|
) : (
|
||||||
srcDoc={srcDoc}
|
<div className="ds-modal-stage-iframe-scaler" style={scalerStyle}>
|
||||||
/>
|
<iframe
|
||||||
)}
|
key={activeView?.id ?? 'view'}
|
||||||
|
title={`${title} ${activeView?.label ?? ''}`}
|
||||||
|
sandbox="allow-scripts allow-same-origin"
|
||||||
|
srcDoc={srcDoc}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{sidebar && !sidebarOpen ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ds-modal-stage-handle is-expand"
|
||||||
|
onClick={() => setSidebarOpen(true)}
|
||||||
|
title={t('preview.showSidebar', { label: sidebar.label })}
|
||||||
|
aria-label={t('preview.showSidebar', { label: sidebar.label })}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">‹</span>
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{sidebar && sidebarOpen ? (
|
||||||
|
<aside className="ds-modal-sidebar" aria-label={sidebar.label}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ds-modal-stage-handle is-collapse"
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
title={t('preview.hideSidebar', { label: sidebar.label })}
|
||||||
|
aria-label={t('preview.hideSidebar', { label: sidebar.label })}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">›</span>
|
||||||
|
</button>
|
||||||
|
{sidebar.content}
|
||||||
|
</aside>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -282,11 +282,13 @@ const DE_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK = [
|
||||||
'agentic',
|
'agentic',
|
||||||
'ant',
|
'ant',
|
||||||
'application',
|
'application',
|
||||||
|
'arc',
|
||||||
'artistic',
|
'artistic',
|
||||||
'bento',
|
'bento',
|
||||||
'bold',
|
'bold',
|
||||||
'brutalism',
|
'brutalism',
|
||||||
'cafe',
|
'cafe',
|
||||||
|
'canva',
|
||||||
'claymorphism',
|
'claymorphism',
|
||||||
'clean',
|
'clean',
|
||||||
'colorful',
|
'colorful',
|
||||||
|
|
@ -295,9 +297,11 @@ const DE_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK = [
|
||||||
'cosmic',
|
'cosmic',
|
||||||
'creative',
|
'creative',
|
||||||
'dashboard',
|
'dashboard',
|
||||||
|
'discord',
|
||||||
'dithered',
|
'dithered',
|
||||||
'doodle',
|
'doodle',
|
||||||
'dramatic',
|
'dramatic',
|
||||||
|
'duolingo',
|
||||||
'editorial',
|
'editorial',
|
||||||
'elegant',
|
'elegant',
|
||||||
'energetic',
|
'energetic',
|
||||||
|
|
@ -307,8 +311,10 @@ const DE_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK = [
|
||||||
'flat',
|
'flat',
|
||||||
'friendly',
|
'friendly',
|
||||||
'futuristic',
|
'futuristic',
|
||||||
|
'github',
|
||||||
'glassmorphism',
|
'glassmorphism',
|
||||||
'gradient',
|
'gradient',
|
||||||
|
'huggingface',
|
||||||
'levels',
|
'levels',
|
||||||
'lingo',
|
'lingo',
|
||||||
'luxury',
|
'luxury',
|
||||||
|
|
@ -319,6 +325,7 @@ const DE_DESIGN_SYSTEM_IDS_WITH_EN_FALLBACK = [
|
||||||
'neobrutalism',
|
'neobrutalism',
|
||||||
'neon',
|
'neon',
|
||||||
'neumorphism',
|
'neumorphism',
|
||||||
|
'openai',
|
||||||
'pacman',
|
'pacman',
|
||||||
'paper',
|
'paper',
|
||||||
'perspective',
|
'perspective',
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,8 @@ export const de: Dict = {
|
||||||
'ds.categoryUncategorized': 'Nicht kategorisiert',
|
'ds.categoryUncategorized': 'Nicht kategorisiert',
|
||||||
'ds.showcase': 'Showcase',
|
'ds.showcase': 'Showcase',
|
||||||
'ds.tokens': 'Tokens',
|
'ds.tokens': 'Tokens',
|
||||||
|
'ds.specToggle': 'DESIGN.md',
|
||||||
|
'ds.specLoading': 'DESIGN.md wird geladen…',
|
||||||
|
|
||||||
'avatar.title': 'Konto & Einstellungen',
|
'avatar.title': 'Konto & Einstellungen',
|
||||||
'avatar.localCli': 'Lokale CLI',
|
'avatar.localCli': 'Lokale CLI',
|
||||||
|
|
@ -405,6 +407,8 @@ export const de: Dict = {
|
||||||
'preview.fullscreen': '⤢ Vollbild',
|
'preview.fullscreen': '⤢ Vollbild',
|
||||||
'preview.closeTitle': 'Schließen (Esc)',
|
'preview.closeTitle': 'Schließen (Esc)',
|
||||||
'preview.loading': '{label} wird geladen…',
|
'preview.loading': '{label} wird geladen…',
|
||||||
|
'preview.showSidebar': '{label} einblenden',
|
||||||
|
'preview.hideSidebar': '{label} ausblenden',
|
||||||
|
|
||||||
'misc.savedTemplate': 'Gespeichertes Template',
|
'misc.savedTemplate': 'Gespeichertes Template',
|
||||||
'misc.primary': 'Primär',
|
'misc.primary': 'Primär',
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,8 @@ export const en: Dict = {
|
||||||
'ds.categoryUncategorized': 'Uncategorized',
|
'ds.categoryUncategorized': 'Uncategorized',
|
||||||
'ds.showcase': 'Showcase',
|
'ds.showcase': 'Showcase',
|
||||||
'ds.tokens': 'Tokens',
|
'ds.tokens': 'Tokens',
|
||||||
|
'ds.specToggle': 'DESIGN.md',
|
||||||
|
'ds.specLoading': 'Loading DESIGN.md…',
|
||||||
|
|
||||||
'avatar.title': 'Account & settings',
|
'avatar.title': 'Account & settings',
|
||||||
'avatar.localCli': 'Local CLI',
|
'avatar.localCli': 'Local CLI',
|
||||||
|
|
@ -405,6 +407,8 @@ export const en: Dict = {
|
||||||
'preview.fullscreen': '⤢ Fullscreen',
|
'preview.fullscreen': '⤢ Fullscreen',
|
||||||
'preview.closeTitle': 'Close (Esc)',
|
'preview.closeTitle': 'Close (Esc)',
|
||||||
'preview.loading': 'Loading {label}…',
|
'preview.loading': 'Loading {label}…',
|
||||||
|
'preview.showSidebar': 'Show {label}',
|
||||||
|
'preview.hideSidebar': 'Hide {label}',
|
||||||
|
|
||||||
'misc.savedTemplate': 'Saved template',
|
'misc.savedTemplate': 'Saved template',
|
||||||
'misc.primary': 'Primary',
|
'misc.primary': 'Primary',
|
||||||
|
|
|
||||||
|
|
@ -311,6 +311,8 @@ export const esES: Dict = {
|
||||||
'ds.categoryUncategorized': 'Sin categoría',
|
'ds.categoryUncategorized': 'Sin categoría',
|
||||||
'ds.showcase': 'Vitrina',
|
'ds.showcase': 'Vitrina',
|
||||||
'ds.tokens': 'Tokens',
|
'ds.tokens': 'Tokens',
|
||||||
|
'ds.specToggle': 'DESIGN.md',
|
||||||
|
'ds.specLoading': 'Cargando DESIGN.md…',
|
||||||
|
|
||||||
'avatar.title': 'Cuenta y ajustes',
|
'avatar.title': 'Cuenta y ajustes',
|
||||||
'avatar.localCli': 'CLI local',
|
'avatar.localCli': 'CLI local',
|
||||||
|
|
@ -406,6 +408,8 @@ export const esES: Dict = {
|
||||||
'preview.fullscreen': '⤢ Pantalla completa',
|
'preview.fullscreen': '⤢ Pantalla completa',
|
||||||
'preview.closeTitle': 'Cerrar (Esc)',
|
'preview.closeTitle': 'Cerrar (Esc)',
|
||||||
'preview.loading': 'Cargando {label}…',
|
'preview.loading': 'Cargando {label}…',
|
||||||
|
'preview.showSidebar': 'Mostrar {label}',
|
||||||
|
'preview.hideSidebar': 'Ocultar {label}',
|
||||||
|
|
||||||
'misc.savedTemplate': 'Plantilla guardada',
|
'misc.savedTemplate': 'Plantilla guardada',
|
||||||
'misc.primary': 'Principal',
|
'misc.primary': 'Principal',
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,8 @@ export const fa: Dict = {
|
||||||
'ds.categoryUncategorized': 'دستهبندی نشده',
|
'ds.categoryUncategorized': 'دستهبندی نشده',
|
||||||
'ds.showcase': 'ویترین',
|
'ds.showcase': 'ویترین',
|
||||||
'ds.tokens': 'توکنها',
|
'ds.tokens': 'توکنها',
|
||||||
|
'ds.specToggle': 'DESIGN.md',
|
||||||
|
'ds.specLoading': 'بارگذاری DESIGN.md…',
|
||||||
|
|
||||||
'avatar.title': 'حساب و تنظیمات',
|
'avatar.title': 'حساب و تنظیمات',
|
||||||
'avatar.localCli': 'CLI محلی',
|
'avatar.localCli': 'CLI محلی',
|
||||||
|
|
@ -405,6 +407,8 @@ export const fa: Dict = {
|
||||||
'preview.fullscreen': '⤢ تمام صفحه',
|
'preview.fullscreen': '⤢ تمام صفحه',
|
||||||
'preview.closeTitle': 'بستن (Esc)',
|
'preview.closeTitle': 'بستن (Esc)',
|
||||||
'preview.loading': 'در حال بارگذاری {label}…',
|
'preview.loading': 'در حال بارگذاری {label}…',
|
||||||
|
'preview.showSidebar': 'نمایش {label}',
|
||||||
|
'preview.hideSidebar': 'پنهان کردن {label}',
|
||||||
|
|
||||||
'misc.savedTemplate': 'قالب ذخیره شده',
|
'misc.savedTemplate': 'قالب ذخیره شده',
|
||||||
'misc.primary': 'اصلی',
|
'misc.primary': 'اصلی',
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,8 @@ export const hu: Dict = {
|
||||||
'ds.categoryUncategorized': 'Kategorizálatlan',
|
'ds.categoryUncategorized': 'Kategorizálatlan',
|
||||||
'ds.showcase': 'Bemutató',
|
'ds.showcase': 'Bemutató',
|
||||||
'ds.tokens': 'Tokenek',
|
'ds.tokens': 'Tokenek',
|
||||||
|
'ds.specToggle': 'DESIGN.md',
|
||||||
|
'ds.specLoading': 'DESIGN.md betöltése…',
|
||||||
|
|
||||||
'avatar.title': 'Fiók és beállítások',
|
'avatar.title': 'Fiók és beállítások',
|
||||||
'avatar.localCli': 'Helyi CLI',
|
'avatar.localCli': 'Helyi CLI',
|
||||||
|
|
@ -405,6 +407,8 @@ export const hu: Dict = {
|
||||||
'preview.fullscreen': '⤢ Teljes képernyő',
|
'preview.fullscreen': '⤢ Teljes képernyő',
|
||||||
'preview.closeTitle': 'Bezárás (Esc)',
|
'preview.closeTitle': 'Bezárás (Esc)',
|
||||||
'preview.loading': '{label} betöltése…',
|
'preview.loading': '{label} betöltése…',
|
||||||
|
'preview.showSidebar': '{label} megjelenítése',
|
||||||
|
'preview.hideSidebar': '{label} elrejtése',
|
||||||
|
|
||||||
'misc.savedTemplate': 'Mentett sablon',
|
'misc.savedTemplate': 'Mentett sablon',
|
||||||
'misc.primary': 'Elsődleges',
|
'misc.primary': 'Elsődleges',
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,8 @@ export const ja: Dict = {
|
||||||
'ds.categoryUncategorized': '未分類',
|
'ds.categoryUncategorized': '未分類',
|
||||||
'ds.showcase': 'ショーケース',
|
'ds.showcase': 'ショーケース',
|
||||||
'ds.tokens': 'トークン',
|
'ds.tokens': 'トークン',
|
||||||
|
'ds.specToggle': 'DESIGN.md',
|
||||||
|
'ds.specLoading': 'DESIGN.md を読み込み中…',
|
||||||
|
|
||||||
'avatar.title': 'アカウントと設定',
|
'avatar.title': 'アカウントと設定',
|
||||||
'avatar.localCli': 'ローカル CLI',
|
'avatar.localCli': 'ローカル CLI',
|
||||||
|
|
@ -404,6 +406,8 @@ export const ja: Dict = {
|
||||||
'preview.fullscreen': '⤢ フルスクリーン',
|
'preview.fullscreen': '⤢ フルスクリーン',
|
||||||
'preview.closeTitle': '閉じる (Esc)',
|
'preview.closeTitle': '閉じる (Esc)',
|
||||||
'preview.loading': '{label} を読み込み中…',
|
'preview.loading': '{label} を読み込み中…',
|
||||||
|
'preview.showSidebar': '{label} を表示',
|
||||||
|
'preview.hideSidebar': '{label} を非表示',
|
||||||
|
|
||||||
'misc.savedTemplate': '保存済みテンプレート',
|
'misc.savedTemplate': '保存済みテンプレート',
|
||||||
'misc.primary': 'プライマリ',
|
'misc.primary': 'プライマリ',
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,8 @@ export const ko: Dict = {
|
||||||
'ds.categoryUncategorized': '미분류',
|
'ds.categoryUncategorized': '미분류',
|
||||||
'ds.showcase': '쇼케이스',
|
'ds.showcase': '쇼케이스',
|
||||||
'ds.tokens': '토큰',
|
'ds.tokens': '토큰',
|
||||||
|
'ds.specToggle': 'DESIGN.md',
|
||||||
|
'ds.specLoading': 'DESIGN.md 불러오는 중…',
|
||||||
|
|
||||||
'avatar.title': '계정 및 설정',
|
'avatar.title': '계정 및 설정',
|
||||||
'avatar.localCli': '로컬 CLI',
|
'avatar.localCli': '로컬 CLI',
|
||||||
|
|
@ -405,6 +407,8 @@ export const ko: Dict = {
|
||||||
'preview.fullscreen': '⤢ 전체 화면',
|
'preview.fullscreen': '⤢ 전체 화면',
|
||||||
'preview.closeTitle': '닫기 (Esc)',
|
'preview.closeTitle': '닫기 (Esc)',
|
||||||
'preview.loading': '{label} 불러오는 중…',
|
'preview.loading': '{label} 불러오는 중…',
|
||||||
|
'preview.showSidebar': '{label} 표시',
|
||||||
|
'preview.hideSidebar': '{label} 숨기기',
|
||||||
|
|
||||||
'misc.savedTemplate': '저장된 템플릿',
|
'misc.savedTemplate': '저장된 템플릿',
|
||||||
'misc.primary': '기본색 (Primary)',
|
'misc.primary': '기본색 (Primary)',
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,8 @@ export const pl: Dict = {
|
||||||
'ds.categoryUncategorized': 'Niekategoryzowane',
|
'ds.categoryUncategorized': 'Niekategoryzowane',
|
||||||
'ds.showcase': 'Galeria',
|
'ds.showcase': 'Galeria',
|
||||||
'ds.tokens': 'Tokeny',
|
'ds.tokens': 'Tokeny',
|
||||||
|
'ds.specToggle': 'DESIGN.md',
|
||||||
|
'ds.specLoading': 'Ładowanie DESIGN.md…',
|
||||||
|
|
||||||
'avatar.title': 'Konto i ustawienia',
|
'avatar.title': 'Konto i ustawienia',
|
||||||
'avatar.localCli': 'Lokalne CLI',
|
'avatar.localCli': 'Lokalne CLI',
|
||||||
|
|
@ -405,6 +407,8 @@ export const pl: Dict = {
|
||||||
'preview.fullscreen': '⤢ Pełny ekran',
|
'preview.fullscreen': '⤢ Pełny ekran',
|
||||||
'preview.closeTitle': 'Zamknij (Esc)',
|
'preview.closeTitle': 'Zamknij (Esc)',
|
||||||
'preview.loading': 'Ładowanie {label}…',
|
'preview.loading': 'Ładowanie {label}…',
|
||||||
|
'preview.showSidebar': 'Pokaż {label}',
|
||||||
|
'preview.hideSidebar': 'Ukryj {label}',
|
||||||
|
|
||||||
'misc.savedTemplate': 'Zapisany szablon',
|
'misc.savedTemplate': 'Zapisany szablon',
|
||||||
'misc.primary': 'Główny',
|
'misc.primary': 'Główny',
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,8 @@ export const ptBR: Dict = {
|
||||||
'ds.categoryUncategorized': 'Sem categoria',
|
'ds.categoryUncategorized': 'Sem categoria',
|
||||||
'ds.showcase': 'Vitrine',
|
'ds.showcase': 'Vitrine',
|
||||||
'ds.tokens': 'Tokens',
|
'ds.tokens': 'Tokens',
|
||||||
|
'ds.specToggle': 'DESIGN.md',
|
||||||
|
'ds.specLoading': 'Carregando DESIGN.md…',
|
||||||
|
|
||||||
'avatar.title': 'Conta e configurações',
|
'avatar.title': 'Conta e configurações',
|
||||||
'avatar.localCli': 'CLI local',
|
'avatar.localCli': 'CLI local',
|
||||||
|
|
@ -404,6 +406,8 @@ export const ptBR: Dict = {
|
||||||
'preview.fullscreen': '⤢ Tela cheia',
|
'preview.fullscreen': '⤢ Tela cheia',
|
||||||
'preview.closeTitle': 'Fechar (Esc)',
|
'preview.closeTitle': 'Fechar (Esc)',
|
||||||
'preview.loading': 'Carregando {label}…',
|
'preview.loading': 'Carregando {label}…',
|
||||||
|
'preview.showSidebar': 'Mostrar {label}',
|
||||||
|
'preview.hideSidebar': 'Ocultar {label}',
|
||||||
|
|
||||||
'misc.savedTemplate': 'Template salvo',
|
'misc.savedTemplate': 'Template salvo',
|
||||||
'misc.primary': 'Principal',
|
'misc.primary': 'Principal',
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,8 @@ export const ru: Dict = {
|
||||||
'ds.categoryUncategorized': 'Без категории',
|
'ds.categoryUncategorized': 'Без категории',
|
||||||
'ds.showcase': 'Витрина',
|
'ds.showcase': 'Витрина',
|
||||||
'ds.tokens': 'Токены',
|
'ds.tokens': 'Токены',
|
||||||
|
'ds.specToggle': 'DESIGN.md',
|
||||||
|
'ds.specLoading': 'Загрузка DESIGN.md…',
|
||||||
|
|
||||||
'avatar.title': 'Аккаунт и настройки',
|
'avatar.title': 'Аккаунт и настройки',
|
||||||
'avatar.localCli': 'Локальный CLI',
|
'avatar.localCli': 'Локальный CLI',
|
||||||
|
|
@ -404,6 +406,8 @@ export const ru: Dict = {
|
||||||
'preview.fullscreen': '⤢ Полноэкранный',
|
'preview.fullscreen': '⤢ Полноэкранный',
|
||||||
'preview.closeTitle': 'Закрыть (Esc)',
|
'preview.closeTitle': 'Закрыть (Esc)',
|
||||||
'preview.loading': 'Загрузка {label}…',
|
'preview.loading': 'Загрузка {label}…',
|
||||||
|
'preview.showSidebar': 'Показать {label}',
|
||||||
|
'preview.hideSidebar': 'Скрыть {label}',
|
||||||
|
|
||||||
'misc.savedTemplate': 'Сохраненный шаблон',
|
'misc.savedTemplate': 'Сохраненный шаблон',
|
||||||
'misc.primary': 'Основной',
|
'misc.primary': 'Основной',
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,8 @@ export const tr: Dict = {
|
||||||
'ds.categoryUncategorized': 'Kategorilendirilmemiş',
|
'ds.categoryUncategorized': 'Kategorilendirilmemiş',
|
||||||
'ds.showcase': 'Tanıtım',
|
'ds.showcase': 'Tanıtım',
|
||||||
'ds.tokens': 'Tokenler',
|
'ds.tokens': 'Tokenler',
|
||||||
|
'ds.specToggle': 'DESIGN.md',
|
||||||
|
'ds.specLoading': 'DESIGN.md yükleniyor…',
|
||||||
|
|
||||||
'avatar.title': 'Hesap & ayarlar',
|
'avatar.title': 'Hesap & ayarlar',
|
||||||
'avatar.localCli': 'Yerel CLI',
|
'avatar.localCli': 'Yerel CLI',
|
||||||
|
|
@ -404,6 +406,8 @@ export const tr: Dict = {
|
||||||
'preview.fullscreen': '⤢ Tam ekran',
|
'preview.fullscreen': '⤢ Tam ekran',
|
||||||
'preview.closeTitle': 'Kapat (Esc)',
|
'preview.closeTitle': 'Kapat (Esc)',
|
||||||
'preview.loading': '{label} yükleniyor…',
|
'preview.loading': '{label} yükleniyor…',
|
||||||
|
'preview.showSidebar': '{label} göster',
|
||||||
|
'preview.hideSidebar': '{label} gizle',
|
||||||
|
|
||||||
'misc.savedTemplate': 'Kaydedilmiş şablonlar',
|
'misc.savedTemplate': 'Kaydedilmiş şablonlar',
|
||||||
'misc.primary': 'Birincil',
|
'misc.primary': 'Birincil',
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,8 @@ export const zhCN: Dict = {
|
||||||
'ds.categoryUncategorized': '未分类',
|
'ds.categoryUncategorized': '未分类',
|
||||||
'ds.showcase': '展示',
|
'ds.showcase': '展示',
|
||||||
'ds.tokens': 'Token',
|
'ds.tokens': 'Token',
|
||||||
|
'ds.specToggle': 'DESIGN.md',
|
||||||
|
'ds.specLoading': '正在加载 DESIGN.md…',
|
||||||
|
|
||||||
'avatar.title': '账户与设置',
|
'avatar.title': '账户与设置',
|
||||||
'avatar.localCli': '本机 CLI',
|
'avatar.localCli': '本机 CLI',
|
||||||
|
|
@ -397,6 +399,8 @@ export const zhCN: Dict = {
|
||||||
'preview.fullscreen': '⤢ 全屏',
|
'preview.fullscreen': '⤢ 全屏',
|
||||||
'preview.closeTitle': '关闭(Esc)',
|
'preview.closeTitle': '关闭(Esc)',
|
||||||
'preview.loading': '正在加载{label}…',
|
'preview.loading': '正在加载{label}…',
|
||||||
|
'preview.showSidebar': '展开{label}',
|
||||||
|
'preview.hideSidebar': '收起{label}',
|
||||||
|
|
||||||
'misc.savedTemplate': '已保存的模板',
|
'misc.savedTemplate': '已保存的模板',
|
||||||
'misc.primary': '主体系',
|
'misc.primary': '主体系',
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,8 @@ export const zhTW: Dict = {
|
||||||
'ds.categoryUncategorized': '未分類',
|
'ds.categoryUncategorized': '未分類',
|
||||||
'ds.showcase': '展示',
|
'ds.showcase': '展示',
|
||||||
'ds.tokens': 'Token',
|
'ds.tokens': 'Token',
|
||||||
|
'ds.specToggle': 'DESIGN.md',
|
||||||
|
'ds.specLoading': '正在載入 DESIGN.md…',
|
||||||
|
|
||||||
'avatar.title': '帳號與設定',
|
'avatar.title': '帳號與設定',
|
||||||
'avatar.localCli': '本機 CLI',
|
'avatar.localCli': '本機 CLI',
|
||||||
|
|
@ -397,6 +399,8 @@ export const zhTW: Dict = {
|
||||||
'preview.fullscreen': '⤢ 全螢幕',
|
'preview.fullscreen': '⤢ 全螢幕',
|
||||||
'preview.closeTitle': '關閉(Esc)',
|
'preview.closeTitle': '關閉(Esc)',
|
||||||
'preview.loading': '正在載入{label}…',
|
'preview.loading': '正在載入{label}…',
|
||||||
|
'preview.showSidebar': '展開{label}',
|
||||||
|
'preview.hideSidebar': '收合{label}',
|
||||||
|
|
||||||
'misc.savedTemplate': '已儲存的範本',
|
'misc.savedTemplate': '已儲存的範本',
|
||||||
'misc.primary': '主系統',
|
'misc.primary': '主系統',
|
||||||
|
|
|
||||||
|
|
@ -329,6 +329,8 @@ export interface Dict {
|
||||||
'ds.categoryUncategorized': string;
|
'ds.categoryUncategorized': string;
|
||||||
'ds.showcase': string;
|
'ds.showcase': string;
|
||||||
'ds.tokens': string;
|
'ds.tokens': string;
|
||||||
|
'ds.specToggle': string;
|
||||||
|
'ds.specLoading': string;
|
||||||
|
|
||||||
// Avatar menu (project topbar)
|
// Avatar menu (project topbar)
|
||||||
'avatar.title': string;
|
'avatar.title': string;
|
||||||
|
|
@ -420,6 +422,8 @@ export interface Dict {
|
||||||
'preview.fullscreen': string;
|
'preview.fullscreen': string;
|
||||||
'preview.closeTitle': string;
|
'preview.closeTitle': string;
|
||||||
'preview.loading': string;
|
'preview.loading': string;
|
||||||
|
'preview.showSidebar': string;
|
||||||
|
'preview.hideSidebar': string;
|
||||||
|
|
||||||
// Misc fallback names
|
// Misc fallback names
|
||||||
'misc.savedTemplate': string;
|
'misc.savedTemplate': string;
|
||||||
|
|
|
||||||
|
|
@ -3058,7 +3058,168 @@ code {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Design systems list */
|
/* Design systems gallery — masonry-style cards with lazy showcase iframes
|
||||||
|
serving as thumbnails. The grid mirrors the prompt-templates and example
|
||||||
|
gallery surfaces so the three browse tabs feel uniform. */
|
||||||
|
.ds-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.ds-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
.ds-card:hover {
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
.ds-card:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
.ds-card.active {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 2px var(--accent-tint);
|
||||||
|
}
|
||||||
|
.ds-card-thumb {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
overflow: hidden;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.ds-card-thumb iframe {
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
border: none;
|
||||||
|
display: block;
|
||||||
|
background: white;
|
||||||
|
transform: scale(0.5);
|
||||||
|
transform-origin: top left;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.ds-card-thumb-fallback {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.ds-card-thumb-swatches {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.ds-card-thumb-swatches > span { display: block; }
|
||||||
|
.ds-card-thumb-overlay {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
background: rgba(15, 15, 18, 0.78);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
.ds-card:hover .ds-card-thumb-overlay,
|
||||||
|
.ds-card-thumb:focus-visible .ds-card-thumb-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.ds-card-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 12px 14px 14px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.ds-card-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.ds-card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
letter-spacing: -0.005em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.ds-card-badge {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent);
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.ds-card-summary {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.45;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.ds-card-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
.ds-card-category {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.ds-card-swatches {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
.ds-card-swatches > span {
|
||||||
|
display: block;
|
||||||
|
width: 14px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.ds-card-swatches > span + span {
|
||||||
|
border-left: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legacy list classes kept for any consumer outside the tab — the gallery
|
||||||
|
itself no longer renders these. Safe to remove once nothing references
|
||||||
|
them. */
|
||||||
.ds-list { display: flex; flex-direction: column; gap: 8px; }
|
.ds-list { display: flex; flex-direction: column; gap: 8px; }
|
||||||
.ds-row {
|
.ds-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -3090,10 +3251,6 @@ code {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.ds-row-summary { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
|
.ds-row-summary { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
|
||||||
|
|
||||||
/* Palette thumbnail next to each design system in the picker. Reads as a
|
|
||||||
tiny brand mark — bg + support + fg + accent. Lets users scan the list
|
|
||||||
visually instead of relying on summary copy alone. */
|
|
||||||
.ds-row-swatches {
|
.ds-row-swatches {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -5534,16 +5691,79 @@ code {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
background: white;
|
background: white;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.ds-modal-stage iframe {
|
.ds-modal-stage-iframe {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.ds-modal-stage-iframe-scaler {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
transform-origin: top left;
|
||||||
|
background: white;
|
||||||
|
/* Prevent the GPU layer from blurring the scaled iframe on Retina. */
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
.ds-modal-stage-iframe-scaler iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
display: block;
|
display: block;
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
.ds-modal-fullscreen .ds-modal-stage:fullscreen iframe,
|
.ds-modal-stage.has-sidebar .ds-modal-stage-iframe {
|
||||||
.ds-modal-stage:fullscreen iframe {
|
flex: 1 1 60%;
|
||||||
|
}
|
||||||
|
.ds-modal-sidebar {
|
||||||
|
position: relative;
|
||||||
|
flex: 1 1 40%;
|
||||||
|
min-width: 320px;
|
||||||
|
max-width: 560px;
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
background: var(--bg-panel);
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.ds-modal-stage-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 18px;
|
||||||
|
height: 56px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
z-index: 3;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||||
|
transition: color 120ms ease, background 120ms ease;
|
||||||
|
}
|
||||||
|
.ds-modal-stage-handle:hover { color: var(--text); background: var(--bg-subtle); }
|
||||||
|
.ds-modal-stage-handle.is-expand {
|
||||||
|
right: 0;
|
||||||
|
border-right: none;
|
||||||
|
border-radius: 8px 0 0 8px;
|
||||||
|
}
|
||||||
|
.ds-modal-stage-handle.is-collapse {
|
||||||
|
left: 0;
|
||||||
|
border-left: none;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
}
|
||||||
|
.ds-modal-fullscreen .ds-modal-stage:fullscreen .ds-modal-stage-iframe-scaler,
|
||||||
|
.ds-modal-stage:fullscreen .ds-modal-stage-iframe-scaler {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
.ds-modal-empty {
|
.ds-modal-empty {
|
||||||
|
|
@ -5555,11 +5775,85 @@ code {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
.ds-modal-actions .ghost.is-active {
|
||||||
|
background: var(--accent-tint);
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DESIGN.md side panel — monospace source view with light syntax tints,
|
||||||
|
echoing the styles.refero.design "compact" markdown source pane. */
|
||||||
|
.design-spec-empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.design-spec-pre {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px 18px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre;
|
||||||
|
overflow: auto;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--bg-panel);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.design-spec-pre code { font: inherit; color: inherit; background: transparent; }
|
||||||
|
.design-spec-line { display: inline; }
|
||||||
|
.design-spec-line.is-h1 { color: #2563eb; font-weight: 700; }
|
||||||
|
.design-spec-line.is-h2 { color: #0891b2; font-weight: 700; }
|
||||||
|
.design-spec-line.is-h3 { color: #0d9488; font-weight: 600; }
|
||||||
|
.design-spec-line.is-h4 { color: #16a34a; font-weight: 600; }
|
||||||
|
.design-spec-line.is-quote { color: #6b7280; font-style: italic; }
|
||||||
|
.design-spec-line.is-list { color: var(--text); }
|
||||||
|
.design-spec-line.is-table { color: #7c3aed; }
|
||||||
|
.design-spec-line.is-fence { color: #dc2626; }
|
||||||
|
.design-spec-line.is-blank { color: var(--text-muted); }
|
||||||
|
.md-tk-bold { font-weight: 700; color: var(--text); }
|
||||||
|
.md-tk-em { font-style: italic; color: var(--text); }
|
||||||
|
.md-tk-code {
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
.md-tk-color {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
color: #be185d;
|
||||||
|
}
|
||||||
|
.md-tk-color-swatch {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.ds-modal-backdrop { padding: 0; }
|
.ds-modal-backdrop { padding: 0; }
|
||||||
.ds-modal { border-radius: 0; }
|
.ds-modal { border-radius: 0; }
|
||||||
.ds-modal-header { grid-template-columns: 1fr; gap: 8px; }
|
.ds-modal-header { grid-template-columns: 1fr; gap: 8px; }
|
||||||
.ds-modal-actions { justify-content: flex-start; flex-wrap: wrap; }
|
.ds-modal-actions { justify-content: flex-start; flex-wrap: wrap; }
|
||||||
|
.ds-modal-stage { flex-direction: column; }
|
||||||
|
.ds-modal-stage.has-sidebar .ds-modal-stage-iframe { flex: 1 1 50%; }
|
||||||
|
.ds-modal-sidebar {
|
||||||
|
border-left: none;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
flex: 1 1 50%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
/* On stacked layout the side handles (which assume horizontal split)
|
||||||
|
would float over content awkwardly — fall back to the header toggle. */
|
||||||
|
.ds-modal-stage-handle { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Examples gallery toolbar — filter pills + richer card metadata */
|
/* Examples gallery toolbar — filter pills + richer card metadata */
|
||||||
|
|
|
||||||
152
design-systems/arc/DESIGN.md
Normal file
152
design-systems/arc/DESIGN.md
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
# Design System Inspired by Arc Browser
|
||||||
|
|
||||||
|
> Category: Productivity & SaaS
|
||||||
|
> "The browser that browses for you." Translucent surfaces, gradient warmth, sidebar-first layout.
|
||||||
|
|
||||||
|
## 1. Visual Theme & Atmosphere
|
||||||
|
|
||||||
|
Arc Browser dissolves the boundary between the chrome and the page. Where Chrome and Safari treat the browser frame as a container, Arc treats it as scenery — the toolbar fades into the system wallpaper, the sidebar carries gradient warmth from the user's chosen "theme color", and translucency is everywhere. The visual signature is **frosted glass plus a single saturated gradient** — most often a peach-to-coral or violet-to-fuchsia bloom — that sets the emotional temperature of the entire window.
|
||||||
|
|
||||||
|
Typography uses **Inter** for chrome and a custom display serif (`Argent CF` or similar) for marketing — when Arc speaks publicly it speaks editorially, in a serif voice unusual for tech. The product itself is sans-only, with tight tracking and generous line-height.
|
||||||
|
|
||||||
|
Shapes are squircle-soft: 12–16px radii on cards, 8px on tabs, 9999px pills for tags. Borders are rare — Arc prefers tinted background washes (`rgba(255, 255, 255, 0.5)` over the gradient) to delineate panes.
|
||||||
|
|
||||||
|
**Key Characteristics:**
|
||||||
|
- Translucent frosted-glass surfaces over a saturated gradient background
|
||||||
|
- Theme-color gradients (peach-coral, violet-fuchsia, mint-cyan) as the primary mood
|
||||||
|
- Inter for product chrome, Argent CF (serif) for marketing display
|
||||||
|
- Squircle-soft 12–16px radii everywhere
|
||||||
|
- Sidebar-first layout: tabs, spaces, and bookmarks live on the left, not the top
|
||||||
|
- Color picker is a brand surface — themes are user-driven, not fixed
|
||||||
|
- Subtle shadows (`0 8px 32px rgba(0,0,0,0.08)`) over the gradient backdrop
|
||||||
|
|
||||||
|
## 2. Color Palette & Roles
|
||||||
|
|
||||||
|
### Primary Theme Gradients (User-selectable; default is "Sunset")
|
||||||
|
- **Sunset Start** (`#ff7e5f`): Peach gradient origin.
|
||||||
|
- **Sunset End** (`#feb47b`): Soft coral gradient terminus.
|
||||||
|
- **Twilight Start** (`#7f5af0`): Violet gradient origin.
|
||||||
|
- **Twilight End** (`#e84393`): Fuchsia gradient terminus.
|
||||||
|
- **Aurora Start** (`#16f2b3`): Mint gradient origin.
|
||||||
|
- **Aurora End** (`#0db4f7`): Cyan gradient terminus.
|
||||||
|
|
||||||
|
### Surface (Frosted)
|
||||||
|
- **Glass Light** (`rgba(255, 255, 255, 0.7)`): Standard frosted pane over gradient.
|
||||||
|
- **Glass Medium** (`rgba(255, 255, 255, 0.5)`): Hover state, tab pill background.
|
||||||
|
- **Glass Heavy** (`rgba(255, 255, 255, 0.85)`): Active pane, command bar.
|
||||||
|
- **Glass Dark** (`rgba(20, 20, 25, 0.6)`): Dark-mode frosted surface.
|
||||||
|
|
||||||
|
### Ink & Text
|
||||||
|
- **Ink Primary** (`#1a1a1f`): Primary text on light frosted surface.
|
||||||
|
- **Ink Secondary** (`#54545a`): Secondary text, tab title at rest.
|
||||||
|
- **Ink Muted** (`#8c8c93`): Tertiary, captions, URL bar.
|
||||||
|
- **Ink Inverse** (`#fafafa`): Text on dark frosted surface.
|
||||||
|
|
||||||
|
### Border & Divider
|
||||||
|
- **Border Glass** (`rgba(255, 255, 255, 0.4)`): Frosted-edge border.
|
||||||
|
- **Border Hairline** (`rgba(0, 0, 0, 0.06)`): Hairline divider on light surface.
|
||||||
|
- **Border Active** (`rgba(0, 0, 0, 0.18)`): Active tab outline.
|
||||||
|
|
||||||
|
### Brand Accent
|
||||||
|
- **Arc Coral** (`#ff5f5f`): Default brand color — used in marketing, `arc.net`.
|
||||||
|
- **Arc Lavender** (`#b794f4`): Secondary brand accent.
|
||||||
|
|
||||||
|
### Semantic
|
||||||
|
- **Success** (`#48bb78`): Toast confirmation.
|
||||||
|
- **Warning** (`#f6ad55`): Permission prompt.
|
||||||
|
- **Error** (`#f56565`): Form validation.
|
||||||
|
|
||||||
|
## 3. Typography Rules
|
||||||
|
|
||||||
|
### Font Family
|
||||||
|
- **Display / Marketing**: `Argent CF`, with fallback: `'Source Serif Pro', Georgia, serif`
|
||||||
|
- **Body / UI**: `Inter`, with fallback: `system-ui, -apple-system, BlinkMacSystemFont, sans-serif`
|
||||||
|
- **Code / Mono**: `Berkeley Mono`, with fallback: `ui-monospace, Menlo, Consolas, monospace`
|
||||||
|
|
||||||
|
### Hierarchy
|
||||||
|
|
||||||
|
| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |
|
||||||
|
|------|------|------|--------|-------------|----------------|-------|
|
||||||
|
| Marketing Hero | Argent CF | 72px (4.5rem) | 400 | 1.05 | -0.03em | Editorial display, marketing only |
|
||||||
|
| Section Heading | Argent CF | 40px (2.5rem) | 400 | 1.15 | -0.02em | Marketing section titles |
|
||||||
|
| Page H1 | Inter | 32px (2rem) | 700 | 1.2 | -0.02em | Settings, command bar header |
|
||||||
|
| Page H2 | Inter | 22px (1.375rem) | 600 | 1.25 | -0.01em | Sub-section |
|
||||||
|
| Tab Title | Inter | 13px (0.8125rem) | 500 | 1.3 | -0.005em | Sidebar tab label |
|
||||||
|
| Body | Inter | 15px (0.9375rem) | 400 | 1.55 | normal | Settings prose, tooltips |
|
||||||
|
| Caption | Inter | 12px (0.75rem) | 500 | 1.4 | 0.01em | URL bar protocol, metadata |
|
||||||
|
| Code | Berkeley Mono | 13px (0.8125rem) | 400 | 1.5 | normal | URL bar, devtools |
|
||||||
|
|
||||||
|
### Principles
|
||||||
|
- **Serif moments are rare**: Argent CF appears only in marketing. The product is sans-only.
|
||||||
|
- **Title size is small**: tabs render at 13px so a long sidebar of 30+ tabs stays scannable.
|
||||||
|
- **Tracking tightens with size**: -0.03em at 72px, returning to normal by 15px.
|
||||||
|
|
||||||
|
## 4. Component Stylings
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
**Primary (Filled)**
|
||||||
|
- Background: linear-gradient on theme color (e.g., `linear-gradient(135deg, #ff7e5f, #feb47b)`)
|
||||||
|
- Text: `#ffffff`
|
||||||
|
- Padding: 10px 20px
|
||||||
|
- Radius: 12px
|
||||||
|
- Shadow: `0 4px 16px rgba(255, 127, 95, 0.3)`
|
||||||
|
- Hover: shadow grows to `0 8px 24px rgba(255, 127, 95, 0.4)`
|
||||||
|
|
||||||
|
**Glass (Secondary)**
|
||||||
|
- Background: `rgba(255, 255, 255, 0.7)`
|
||||||
|
- Backdrop: `blur(20px)`
|
||||||
|
- Text: `#1a1a1f`
|
||||||
|
- Border: 1px solid `rgba(255, 255, 255, 0.4)`
|
||||||
|
- Padding: 10px 20px
|
||||||
|
- Radius: 12px
|
||||||
|
|
||||||
|
**Subtle**
|
||||||
|
- Background: transparent
|
||||||
|
- Text: theme color
|
||||||
|
- Hover: background `rgba(255, 127, 95, 0.1)`
|
||||||
|
|
||||||
|
### Tabs (Sidebar)
|
||||||
|
- Background at rest: transparent
|
||||||
|
- Background on hover: `rgba(255, 255, 255, 0.5)`
|
||||||
|
- Background active: `rgba(255, 255, 255, 0.85)` + soft shadow
|
||||||
|
- Padding: 8px 12px
|
||||||
|
- Radius: 8px
|
||||||
|
- Favicon: 16px square at left, 8px gap to title.
|
||||||
|
|
||||||
|
### Cards / Panes
|
||||||
|
- Background: `rgba(255, 255, 255, 0.7)`
|
||||||
|
- Backdrop: `blur(24px)` saturate 180%
|
||||||
|
- Border: 1px solid `rgba(255, 255, 255, 0.4)`
|
||||||
|
- Radius: 16px
|
||||||
|
- Shadow: `0 8px 32px rgba(0, 0, 0, 0.08)`
|
||||||
|
- Padding: 24px
|
||||||
|
|
||||||
|
### Inputs (Command Bar)
|
||||||
|
- Background: `rgba(255, 255, 255, 0.85)`
|
||||||
|
- Backdrop: `blur(40px)`
|
||||||
|
- Text: `#1a1a1f`
|
||||||
|
- Border: 1px solid `rgba(255, 255, 255, 0.4)`
|
||||||
|
- Radius: 14px
|
||||||
|
- Padding: 14px 18px
|
||||||
|
- Focus: shadow `0 0 0 4px rgba(255, 127, 95, 0.2)`
|
||||||
|
|
||||||
|
### Pills (Spaces / Bookmarks Folder)
|
||||||
|
- Background: theme color at 16% alpha
|
||||||
|
- Text: theme color (full)
|
||||||
|
- Padding: 4px 10px
|
||||||
|
- Radius: 9999px
|
||||||
|
- Font: 12px / 600
|
||||||
|
|
||||||
|
## 5. Spacing & Layout
|
||||||
|
|
||||||
|
- **Base unit**: 4px. Scale: 4, 8, 12, 16, 24, 32, 48, 64.
|
||||||
|
- **Sidebar**: 240px wide; collapsible to 56px.
|
||||||
|
- **Window radius**: 12px on the OS window itself (macOS-only flourish).
|
||||||
|
- **Padding inside panes**: 24px.
|
||||||
|
|
||||||
|
## 6. Motion
|
||||||
|
|
||||||
|
- **Duration**: 200ms for hover; 320ms for tab create/close; 480ms for "Little Arc" window expand.
|
||||||
|
- **Easing**: `cubic-bezier(0.32, 0.72, 0, 1)` for window expand (Apple's spring-style).
|
||||||
|
- **Tab swap**: 1px translate + opacity blend, no scale change.
|
||||||
157
design-systems/canva/DESIGN.md
Normal file
157
design-systems/canva/DESIGN.md
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
# Design System Inspired by Canva
|
||||||
|
|
||||||
|
> Category: Design & Creative
|
||||||
|
> Visual creation platform. Vivid purple-blue gradient, generous spacing, friendly geometry.
|
||||||
|
|
||||||
|
## 1. Visual Theme & Atmosphere
|
||||||
|
|
||||||
|
Canva is the friendly face of design tools — the brand makes a point of looking inviting where Adobe looks intimidating. The page is built on a clean white canvas (`#ffffff`) with a signature **purple-to-blue gradient** (`#7d2ae8` → `#00c4cc`) used in the brand mark, hero buttons, and Pro/Magic moments. Surfaces are generously padded, edges are gently rounded (8–16px), and shadows are soft and cool-toned.
|
||||||
|
|
||||||
|
Typography uses **Canva Sans** (a custom geometric sans) for chrome and prose, with rounded letterforms that share DNA with brands like Airbnb and Asana. Weight contrast does the heavy lifting — 800 for hero display, 700 for section heads, 400 for body — while size hierarchy is more compressed than typical product brands so cards and templates read at a glance.
|
||||||
|
|
||||||
|
The shape system is ultra-soft: 12px on most cards, 16–20px on larger panels, 9999px on chips. Buttons are rectangles with a subtle elevation shadow (`0 2px 8px rgba(0,0,0,0.06)`) that grows on hover. Iconography is filled and rounded, never line-only — Canva's icons speak the same shape language as its UI.
|
||||||
|
|
||||||
|
**Key Characteristics:**
|
||||||
|
- White canvas with a violet-to-cyan gradient (`#7d2ae8` → `#00c4cc`)
|
||||||
|
- Canva Sans (rounded geometric) for everything; weight contrast over color
|
||||||
|
- 12–20px radii everywhere; 9999px pills for chips and tags
|
||||||
|
- Soft cool-toned shadows that grow on hover
|
||||||
|
- Filled rounded iconography — never outlined
|
||||||
|
- Vibrant secondary palette (coral, mint, tangerine) for category tags
|
||||||
|
- Pro/Magic moments lit by a static gradient — no animation
|
||||||
|
|
||||||
|
## 2. Color Palette & Roles
|
||||||
|
|
||||||
|
### Brand Gradient
|
||||||
|
- **Canva Purple** (`#7d2ae8`): Brand primary; gradient origin.
|
||||||
|
- **Canva Cyan** (`#00c4cc`): Brand secondary; gradient terminus.
|
||||||
|
- **Canva Pink** (`#ff5757`): Tertiary brand accent (Magic Studio).
|
||||||
|
|
||||||
|
### Surface
|
||||||
|
- **Canvas** (`#ffffff`): Primary background.
|
||||||
|
- **Surface Subtle** (`#f4f5f7`): Section break, sidebar.
|
||||||
|
- **Surface Inset** (`#e8eaed`): Disabled, inset block.
|
||||||
|
- **Surface Cool** (`#eef0fc`): Hover tint on purple-themed cards.
|
||||||
|
|
||||||
|
### Ink & Text
|
||||||
|
- **Ink Primary** (`#0e1318`): Primary text.
|
||||||
|
- **Ink Secondary** (`#3c4043`): Body prose.
|
||||||
|
- **Ink Muted** (`#5f6368`): Captions, descriptions.
|
||||||
|
- **Ink Faint** (`#9aa0a6`): Placeholder, disabled label.
|
||||||
|
|
||||||
|
### Semantic
|
||||||
|
- **Success** (`#00b894`): Saved, exported.
|
||||||
|
- **Warning** (`#ffb020`): Storage limit, advisory.
|
||||||
|
- **Error** (`#ff5757`): Validation, destructive.
|
||||||
|
- **Info** (`#0d99ff`): Tip, link.
|
||||||
|
|
||||||
|
### Category Accents (Template Tags)
|
||||||
|
- **Coral** (`#ff7059`): Social posts.
|
||||||
|
- **Tangerine** (`#ff9633`): Marketing.
|
||||||
|
- **Mint** (`#48c997`): Education.
|
||||||
|
- **Sky** (`#3ea6ff`): Business.
|
||||||
|
- **Lavender** (`#9b87f5`): Personal.
|
||||||
|
|
||||||
|
### Border
|
||||||
|
- **Border Default** (`#e1e3e6`): Standard hairline.
|
||||||
|
- **Border Strong** (`#c7cdd3`): Emphasized border, hover state.
|
||||||
|
|
||||||
|
## 3. Typography Rules
|
||||||
|
|
||||||
|
### Font Family
|
||||||
|
- **Display / UI / Body**: `Canva Sans`, with fallback: `'YS Text', system-ui, -apple-system, sans-serif`
|
||||||
|
- **Editorial (rare)**: `Canva Sans Display`, with fallback: `'Canva Sans', sans-serif`
|
||||||
|
- **Code (devtools only)**: `JetBrains Mono`, with fallback: `ui-monospace, Menlo, Consolas, monospace`
|
||||||
|
|
||||||
|
### Hierarchy
|
||||||
|
|
||||||
|
| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |
|
||||||
|
|------|------|------|--------|-------------|----------------|-------|
|
||||||
|
| Hero | Canva Sans | 64px (4rem) | 800 | 1.05 | -0.02em | Marketing hero, "Design anything." |
|
||||||
|
| H1 | Canva Sans | 36px (2.25rem) | 700 | 1.15 | -0.01em | Page heading |
|
||||||
|
| H2 | Canva Sans | 24px (1.5rem) | 700 | 1.2 | -0.005em | Section heading |
|
||||||
|
| H3 | Canva Sans | 18px (1.125rem) | 600 | 1.3 | normal | Sub-section, card title |
|
||||||
|
| Body Large | Canva Sans | 16px (1rem) | 400 | 1.55 | normal | Lede, marketing body |
|
||||||
|
| Body | Canva Sans | 14px (0.875rem) | 400 | 1.5 | normal | Standard UI prose |
|
||||||
|
| Caption | Canva Sans | 12px (0.75rem) | 500 | 1.4 | 0.005em | Metadata, hint text |
|
||||||
|
| Button | Canva Sans | 14px (0.875rem) | 600 | 1.2 | normal | Default button label |
|
||||||
|
| Tag | Canva Sans | 11px (0.6875rem) | 600 | 1.2 | 0.04em | Uppercase category chip |
|
||||||
|
|
||||||
|
### Principles
|
||||||
|
- **Weight contrast over color contrast**: hierarchy steps via 800→700→600→400; the surface stays neutral.
|
||||||
|
- **Tight line-height for cards**: card titles use 1.15–1.2 so a 3-line title still fits a 4:3 thumbnail.
|
||||||
|
- **No display serif**: Canva is sans-only across all surfaces; serifs appear only as user-selectable template fonts inside the editor.
|
||||||
|
|
||||||
|
## 4. Component Stylings
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
**Primary (Gradient)**
|
||||||
|
- Background: `linear-gradient(135deg, #7d2ae8, #00c4cc)`
|
||||||
|
- Text: `#ffffff`
|
||||||
|
- Padding: 12px 20px
|
||||||
|
- Radius: 8px
|
||||||
|
- Shadow: `0 2px 8px rgba(125, 42, 232, 0.2)`
|
||||||
|
- Hover: shadow grows to `0 4px 14px rgba(125, 42, 232, 0.3)`
|
||||||
|
- Use: hero CTAs, "Try Canva Pro"
|
||||||
|
|
||||||
|
**Primary (Solid Purple)**
|
||||||
|
- Background: `#7d2ae8`
|
||||||
|
- Text: `#ffffff`
|
||||||
|
- Padding: 12px 20px
|
||||||
|
- Radius: 8px
|
||||||
|
- Hover: `#6815d4`
|
||||||
|
|
||||||
|
**Secondary**
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Text: `#0e1318`
|
||||||
|
- Border: 1px solid `#e1e3e6`
|
||||||
|
- Radius: 8px
|
||||||
|
- Hover: background `#f4f5f7`, border `#c7cdd3`
|
||||||
|
|
||||||
|
**Subtle / Tertiary**
|
||||||
|
- Background: `rgba(125, 42, 232, 0.08)`
|
||||||
|
- Text: `#7d2ae8`
|
||||||
|
- Hover: background `rgba(125, 42, 232, 0.14)`
|
||||||
|
|
||||||
|
### Cards / Template Tiles
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Border: 1px solid `#e1e3e6`
|
||||||
|
- Radius: 12px
|
||||||
|
- Shadow at rest: `0 1px 3px rgba(0,0,0,0.04)`
|
||||||
|
- Shadow on hover: `0 8px 24px rgba(0,0,0,0.08)`, lift 2px
|
||||||
|
- Aspect ratio: thumbnail respects template (1:1, 4:3, 9:16)
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Border: 1px solid `#e1e3e6`
|
||||||
|
- Radius: 8px
|
||||||
|
- Padding: 10px 14px
|
||||||
|
- Focus: border `#7d2ae8`, ring `0 0 0 3px rgba(125, 42, 232, 0.16)`
|
||||||
|
|
||||||
|
### Chips / Tags
|
||||||
|
- Background: category-tinted soft.
|
||||||
|
- Text: matching strong category color.
|
||||||
|
- Padding: 4px 10px
|
||||||
|
- Radius: 9999px
|
||||||
|
- Font: 11px / 600 / uppercase
|
||||||
|
|
||||||
|
### Pro Badge
|
||||||
|
- Background: `linear-gradient(135deg, #7d2ae8, #ff5757)`
|
||||||
|
- Text: `#ffffff`
|
||||||
|
- Padding: 2px 8px
|
||||||
|
- Radius: 9999px
|
||||||
|
- Font: 10px / 700 / uppercase
|
||||||
|
|
||||||
|
## 5. Spacing & Layout
|
||||||
|
|
||||||
|
- **Base unit**: 4px. Scale: 4, 8, 12, 16, 24, 32, 48, 64, 96.
|
||||||
|
- **Container**: max 1320px, 32px gutter.
|
||||||
|
- **Sidebar (editor)**: 320px wide; collapses to 56px icons.
|
||||||
|
- **Card grid gap**: 16px on mobile, 24px on desktop.
|
||||||
|
|
||||||
|
## 6. Motion
|
||||||
|
|
||||||
|
- **Duration**: 180ms for hover; 280ms for menu open; 420ms for editor sidebar collapse.
|
||||||
|
- **Easing**: `cubic-bezier(0.4, 0, 0.2, 1)` (Material-style).
|
||||||
|
- **Card lift**: translateY(-2px) + shadow grow on hover, single transition.
|
||||||
162
design-systems/discord/DESIGN.md
Normal file
162
design-systems/discord/DESIGN.md
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
# Design System Inspired by Discord
|
||||||
|
|
||||||
|
> Category: Productivity & SaaS
|
||||||
|
> Voice / chat platform. Deep blurple, dark-first surfaces, playful accent moments.
|
||||||
|
|
||||||
|
## 1. Visual Theme & Atmosphere
|
||||||
|
|
||||||
|
Discord's product is engineered for evenings, raids, and group voice — so the entire surface is dark-first. The default canvas is the deep `Background Primary` (`#313338` light theme, `#1e1f22` dark theme), with chat columns layered on slightly lighter or darker shades to denote channels, threads, and side panels. The signature **Blurple** (`#5865f2`) is reserved for the brand mark, primary CTAs, mentions, and the "you" affordance — used sparingly so it pops against the muted neutrals.
|
||||||
|
|
||||||
|
Typography is **gg sans** (Discord's custom Whitney-replacement) for prose and chrome, with rounded geometric shapes that feel approachable but still legible at the small sizes a chat client demands. Headings step up incrementally; chat rows are tight (4–8px between message groups) so hours of scrollback feel scannable.
|
||||||
|
|
||||||
|
The shape language is rounded but not balloon-soft: 8px radii on cards, 4px on inputs, full pills on status badges and tags. Servers are rounded-square avatars at 48px that morph to circles on hover — a tiny piece of motion that has become part of the brand's identity.
|
||||||
|
|
||||||
|
**Key Characteristics:**
|
||||||
|
- Dark-first surfaces: `#1e1f22` / `#2b2d31` / `#313338` (3-step depth)
|
||||||
|
- Blurple `#5865f2` as the only saturated accent in the chat surface
|
||||||
|
- gg sans (Whitney-style) for all text — friendly, geometric, neutral
|
||||||
|
- Rounded-square server avatars (16px radius) that snap to circles on hover
|
||||||
|
- Tight chat-row spacing, generous side-panel padding
|
||||||
|
- Status dots: green online, yellow idle, red dnd, gray offline
|
||||||
|
- Pixel-snapped 1px dividers in subtle off-white at low alpha
|
||||||
|
|
||||||
|
## 2. Color Palette & Roles
|
||||||
|
|
||||||
|
### Primary
|
||||||
|
- **Blurple** (`#5865f2`): Brand primary, primary CTA, mention highlight.
|
||||||
|
- **Blurple Hover** (`#4752c4`): Hover/active for blurple.
|
||||||
|
- **Blurple Soft** (`#7289da`): Legacy blurple, secondary accent in marketing.
|
||||||
|
|
||||||
|
### Surface (Dark Theme — default)
|
||||||
|
- **Background Tertiary** (`#1e1f22`): Server list rail, deepest background.
|
||||||
|
- **Background Secondary** (`#2b2d31`): Channel sidebar, settings sidebar.
|
||||||
|
- **Background Primary** (`#313338`): Chat surface, message column.
|
||||||
|
- **Background Floating** (`#111214`): Floating popovers, tooltips, autocomplete.
|
||||||
|
- **Background Modifier Hover** (`rgba(78, 80, 88, 0.3)`): Hover overlay on rows.
|
||||||
|
- **Background Modifier Selected** (`rgba(78, 80, 88, 0.6)`): Active row.
|
||||||
|
|
||||||
|
### Surface (Light Theme)
|
||||||
|
- **Light Bg Primary** (`#ffffff`): Chat surface in light theme.
|
||||||
|
- **Light Bg Secondary** (`#f2f3f5`): Sidebar in light theme.
|
||||||
|
- **Light Bg Tertiary** (`#e3e5e8`): Deepest light surface.
|
||||||
|
|
||||||
|
### Text
|
||||||
|
- **Header Primary** (`#f2f3f5`): Channel headers, modal titles in dark theme.
|
||||||
|
- **Header Secondary** (`#b5bac1`): Muted headers.
|
||||||
|
- **Text Normal** (`#dbdee1`): Body text in dark theme — slightly cooler than pure white.
|
||||||
|
- **Text Muted** (`#949ba4`): Timestamps, server names, secondary metadata.
|
||||||
|
- **Text Link** (`#00a8fc`): Hyperlinks in messages — sky blue, distinct from blurple.
|
||||||
|
- **Channels Default** (`#80848e`): Inactive channel name in sidebar.
|
||||||
|
|
||||||
|
### Status & Semantic
|
||||||
|
- **Status Online** (`#23a55a`): Online dot, success states.
|
||||||
|
- **Status Idle** (`#f0b232`): Idle dot, away.
|
||||||
|
- **Status DND** (`#f23f43`): Do-not-disturb, also serves as destructive red.
|
||||||
|
- **Status Streaming** (`#593695`): "Streaming" purple.
|
||||||
|
- **Status Offline** (`#80848e`): Offline gray.
|
||||||
|
- **Mention Highlight** (`rgba(88, 101, 242, 0.1)`): Soft blurple wash on @mention rows.
|
||||||
|
|
||||||
|
### Border & Divider
|
||||||
|
- **Background Modifier Accent** (`rgba(255, 255, 255, 0.06)`): Standard divider in dark.
|
||||||
|
- **Border Subtle** (`#3f4147`): Solid divider for cards.
|
||||||
|
|
||||||
|
## 3. Typography Rules
|
||||||
|
|
||||||
|
### Font Family
|
||||||
|
- **Body / UI / Headings**: `gg sans`, with fallback: `"Helvetica Neue", Helvetica, Arial, sans-serif`
|
||||||
|
- **Display (legacy / Whitney)**: `Whitney`, with fallback: `gg sans`
|
||||||
|
- **Code / Mono**: `"gg mono"`, with fallback: `Consolas, Andale Mono, Courier New, Courier, monospace`
|
||||||
|
|
||||||
|
### Hierarchy
|
||||||
|
|
||||||
|
| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |
|
||||||
|
|------|------|------|--------|-------------|----------------|-------|
|
||||||
|
| Display Hero | gg sans | 56px (3.5rem) | 800 | 1.1 | -0.02em | Marketing hero |
|
||||||
|
| Page Heading | gg sans | 24px (1.5rem) | 700 | 1.25 | normal | Settings/profile titles |
|
||||||
|
| Channel Name | gg sans | 16px (1rem) | 600 | 1.25 | normal | `#general`, channel header |
|
||||||
|
| Message Body | gg sans | 16px (1rem) | 400 | 1.375 | normal | Standard chat text |
|
||||||
|
| Username | gg sans | 16px (1rem) | 500 | 1.25 | normal | Author of a message |
|
||||||
|
| Timestamp | gg sans | 12px (0.75rem) | 500 | 1.25 | normal | "Today at 4:32 PM" |
|
||||||
|
| Sidebar Channel | gg sans | 16px (1rem) | 500 | 1.25 | normal | Channel list rows |
|
||||||
|
| Server Name | gg sans | 16px (1rem) | 600 | 1.25 | normal | Server header |
|
||||||
|
| Caption / Meta | gg sans | 12px (0.75rem) | 400 | 1.3 | 0.02em | Status text, edited tag |
|
||||||
|
| Code Inline | gg mono | 0.875em | 400 | inherit | normal | Inline `code` |
|
||||||
|
| Code Block | gg mono | 14px (0.875rem) | 400 | 1.5 | normal | ```triple-fenced``` block |
|
||||||
|
|
||||||
|
### Principles
|
||||||
|
- **Friendly geometry**: gg sans replaces Whitney with rounded terminals on a/g/s — the brand wants warmth without breaking legibility.
|
||||||
|
- **Weight contrast over color contrast**: hierarchy comes from 400→500→600→700→800 weight steps; the surface stays neutral.
|
||||||
|
- **16px body**: chat messages do not shrink below 16px. Density comes from line-height (1.375), not font size.
|
||||||
|
|
||||||
|
## 4. Component Stylings
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
**Primary**
|
||||||
|
- Background: `#5865f2`
|
||||||
|
- Text: `#ffffff`
|
||||||
|
- Padding: 8px 16px
|
||||||
|
- Radius: 4px
|
||||||
|
- Hover: `#4752c4`
|
||||||
|
- Use: Primary CTAs, "Continue", "Join Server"
|
||||||
|
|
||||||
|
**Secondary**
|
||||||
|
- Background: `#4e5058`
|
||||||
|
- Text: `#ffffff`
|
||||||
|
- Padding: 8px 16px
|
||||||
|
- Radius: 4px
|
||||||
|
- Hover: `#6d6f78`
|
||||||
|
|
||||||
|
**Tertiary / Subtle (Link-style)**
|
||||||
|
- Background: transparent
|
||||||
|
- Text: `#dbdee1`
|
||||||
|
- Hover: text underlined, no background change
|
||||||
|
|
||||||
|
**Danger**
|
||||||
|
- Background: `#da373c`
|
||||||
|
- Text: `#ffffff`
|
||||||
|
- Hover: `#a12d2f`
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- Background: `#1e1f22`
|
||||||
|
- Text: `#dbdee1`
|
||||||
|
- Border: 1px solid `#1e1f22`
|
||||||
|
- Radius: 4px
|
||||||
|
- Padding: 10px 12px
|
||||||
|
- Focus: border `#5865f2`
|
||||||
|
|
||||||
|
### Server Avatars
|
||||||
|
- Size: 48×48px
|
||||||
|
- Radius: 16px (rounded square) by default; transitions to 50% on hover and active.
|
||||||
|
- Active state: 4px white pill on the left edge of the icon column.
|
||||||
|
|
||||||
|
### Status Dots
|
||||||
|
- Size: 10×10px
|
||||||
|
- Border: 3px solid background-tertiary (creates the "notch" effect)
|
||||||
|
- Position: bottom-right of avatar.
|
||||||
|
|
||||||
|
### Cards / Embeds
|
||||||
|
- Background: `#2b2d31` (dark) or `#f2f3f5` (light)
|
||||||
|
- Left border: 4px solid embed accent color.
|
||||||
|
- Radius: 4px
|
||||||
|
- Padding: 8px 16px
|
||||||
|
|
||||||
|
### Mention Pill
|
||||||
|
- Background: `rgba(88, 101, 242, 0.3)`
|
||||||
|
- Text: `#c9cdfb`
|
||||||
|
- Padding: 0 2px
|
||||||
|
- Radius: 3px
|
||||||
|
|
||||||
|
## 5. Spacing & Layout
|
||||||
|
|
||||||
|
- **Base unit**: 4px. Scale: 4, 8, 12, 16, 20, 24, 32, 40.
|
||||||
|
- **Server rail**: 72px wide, fixed.
|
||||||
|
- **Channel sidebar**: 240px wide.
|
||||||
|
- **Member list**: 240px wide on desktop.
|
||||||
|
- **Chat column**: fluid, min 380px.
|
||||||
|
|
||||||
|
## 6. Motion
|
||||||
|
|
||||||
|
- **Duration**: 200ms for hover; 350ms for the avatar circle-morph; 80ms for tooltip fade.
|
||||||
|
- **Easing**: `cubic-bezier(0.215, 0.61, 0.355, 1)` for the avatar morph (snappy then settle).
|
||||||
|
- **Notification pulse**: 1.4s ease-in-out infinite on unread mention indicator.
|
||||||
154
design-systems/duolingo/DESIGN.md
Normal file
154
design-systems/duolingo/DESIGN.md
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
# Design System Inspired by Duolingo
|
||||||
|
|
||||||
|
> Category: Productivity & SaaS
|
||||||
|
> Language-learning platform. Bright owl green, chunky shadows, gamified joy.
|
||||||
|
|
||||||
|
## 1. Visual Theme & Atmosphere
|
||||||
|
|
||||||
|
Duolingo is gamification rendered as visual language. The interface is unapologetically bright, with **owl green** (`#58cc02`) as the brand primary and a chunky 4px bottom-shadow on every interactive element that reads like a 3D button waiting to be pressed. The page is white (`#ffffff`) with thick 2–3px borders in a deep gray (`#e5e5e5`) and the entire system reads like an iOS app from 2015 reborn with better hierarchy.
|
||||||
|
|
||||||
|
Typography uses **Feather Bold** (a custom rounded sans) for chrome and **Mona Sans** (or Inter) for body. Display sizes are big and confident — Duolingo never whispers. Headings often carry the green underline-stroke or sit on a green pill, and the mascot Duo (a green owl) appears as an active illustration character, not a static logo.
|
||||||
|
|
||||||
|
Shape language is friendly: 16–20px radii on cards, 12px on buttons, 9999px on chips and progress bars. Iconography is filled, rounded, and color-coded by skill — every lesson surface has an instantly identifiable color pairing.
|
||||||
|
|
||||||
|
**Key Characteristics:**
|
||||||
|
- Owl green (`#58cc02`) as the dominant brand color, used in 30%+ of the surface
|
||||||
|
- Chunky 4px bottom-shadow on every button (the "tactile press" affordance)
|
||||||
|
- 2–3px solid borders, never hairlines
|
||||||
|
- Feather Bold (rounded display) + Mona Sans (body)
|
||||||
|
- Big confident type — display sizes start at 48px and climb
|
||||||
|
- Mascot-as-character: Duo the owl appears in onboarding, errors, streaks
|
||||||
|
- Streak orange (`#ff9600`) and gem pink (`#ce82ff`) as secondary brand colors
|
||||||
|
|
||||||
|
## 2. Color Palette & Roles
|
||||||
|
|
||||||
|
### Primary
|
||||||
|
- **Owl Green** (`#58cc02`): Brand primary, primary CTA, correct answer.
|
||||||
|
- **Owl Green Deep** (`#58a700`): Pressed/shadow color for green buttons.
|
||||||
|
- **Owl Green Light** (`#89e219`): Hover, soft fills.
|
||||||
|
- **Owl Green Pale** (`#dbf8c5`): Soft surface, success banner.
|
||||||
|
|
||||||
|
### Secondary Accents
|
||||||
|
- **Streak Orange** (`#ff9600`): Streak counter, fire icon, premium energy.
|
||||||
|
- **Streak Orange Deep** (`#cc7a00`): Pressed orange.
|
||||||
|
- **Gem Pink** (`#ce82ff`): Gem currency, Super Duolingo.
|
||||||
|
- **Eel Blue** (`#1cb0f6`): Hint button, info link.
|
||||||
|
- **Cardinal Red** (`#ff4b4b`): Wrong answer, life lost.
|
||||||
|
- **Bee Yellow** (`#ffc800`): Pro badge, achievement.
|
||||||
|
|
||||||
|
### Surface
|
||||||
|
- **Snow** (`#ffffff`): Primary background.
|
||||||
|
- **Eel** (`#f7f7f7`): Section break, secondary surface.
|
||||||
|
- **Swan** (`#e5e5e5`): Disabled background, inset block.
|
||||||
|
- **Wolf** (`#777777`): Dark divider, secondary text.
|
||||||
|
|
||||||
|
### Ink & Text
|
||||||
|
- **Eel Black** (`#3c3c3c`): Primary text.
|
||||||
|
- **Wolf** (`#777777`): Secondary text, captions.
|
||||||
|
- **Hare** (`#afafaf`): Disabled, placeholder.
|
||||||
|
|
||||||
|
### Border
|
||||||
|
- **Swan** (`#e5e5e5`): Standard 2px border.
|
||||||
|
- **Hare** (`#afafaf`): Emphasized border on hover.
|
||||||
|
|
||||||
|
## 3. Typography Rules
|
||||||
|
|
||||||
|
### Font Family
|
||||||
|
- **Display / UI / Headings**: `Feather Bold`, with fallback: `'DIN Round Pro', 'Helvetica Neue', sans-serif`
|
||||||
|
- **Body / Long-form**: `Mona Sans`, with fallback: `'Helvetica Neue', system-ui, sans-serif`
|
||||||
|
- **Code (rare, schools/admin)**: `JetBrains Mono`, with fallback: `ui-monospace, Menlo, monospace`
|
||||||
|
|
||||||
|
### Hierarchy
|
||||||
|
|
||||||
|
| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |
|
||||||
|
|------|------|------|--------|-------------|----------------|-------|
|
||||||
|
| Display | Feather Bold | 56px (3.5rem) | 800 | 1.05 | -0.01em | Onboarding hero |
|
||||||
|
| H1 | Feather Bold | 32px (2rem) | 800 | 1.15 | -0.005em | Page title |
|
||||||
|
| H2 | Feather Bold | 24px (1.5rem) | 800 | 1.2 | normal | Section heading |
|
||||||
|
| H3 | Feather Bold | 18px (1.125rem) | 700 | 1.25 | normal | Card title, lesson row |
|
||||||
|
| Body Large | Mona Sans | 17px (1.0625rem) | 500 | 1.5 | normal | Lesson prompt, instruction |
|
||||||
|
| Body | Mona Sans | 15px (0.9375rem) | 400 | 1.5 | normal | Standard prose |
|
||||||
|
| Caption | Mona Sans | 13px (0.8125rem) | 600 | 1.4 | 0.01em | XP counter, metadata |
|
||||||
|
| Button | Feather Bold | 16px (1rem) | 800 | 1.2 | 0.02em | Standard button label |
|
||||||
|
| Streak | Feather Bold | 14px (0.875rem) | 800 | 1.2 | normal | Streak number, on flame |
|
||||||
|
|
||||||
|
### Principles
|
||||||
|
- **800 is default**: Feather Bold runs at 800 across headings and buttons. 700 feels weak in this system.
|
||||||
|
- **Big type**: heading sizes are 25–40% larger than typical product brands — confidence as identity.
|
||||||
|
- **Rounded letterforms**: every glyph has soft terminals; sharp serifs would break the friendliness contract.
|
||||||
|
|
||||||
|
## 4. Component Stylings
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
**Primary (Owl Green)**
|
||||||
|
- Background: `#58cc02`
|
||||||
|
- Text: `#ffffff`
|
||||||
|
- Padding: 14px 24px
|
||||||
|
- Radius: 16px
|
||||||
|
- Border-bottom: 4px solid `#58a700` (the chunky shadow)
|
||||||
|
- Hover: background `#89e219`
|
||||||
|
- Active: translate-y 4px, border-bottom 0 (button "presses")
|
||||||
|
- Use: "Continue", "Check", main CTA.
|
||||||
|
|
||||||
|
**Secondary (White with Bottom-Shadow)**
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Text: `#777777`
|
||||||
|
- Border: 2px solid `#e5e5e5`
|
||||||
|
- Border-bottom: 4px solid `#e5e5e5`
|
||||||
|
- Radius: 16px
|
||||||
|
- Padding: 14px 24px
|
||||||
|
- Hover: text `#3c3c3c`, border `#afafaf`
|
||||||
|
|
||||||
|
**Streak Orange**
|
||||||
|
- Background: `#ff9600`
|
||||||
|
- Text: `#ffffff`
|
||||||
|
- Border-bottom: 4px solid `#cc7a00`
|
||||||
|
- Use: streak goal, "Start streak"
|
||||||
|
|
||||||
|
**Error (Cardinal Red)**
|
||||||
|
- Background: `#ff4b4b`
|
||||||
|
- Text: `#ffffff`
|
||||||
|
- Border-bottom: 4px solid `#cc3b3b`
|
||||||
|
- Use: wrong answer feedback.
|
||||||
|
|
||||||
|
### Cards / Lesson Tiles
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Border: 2px solid `#e5e5e5`
|
||||||
|
- Border-bottom: 4px solid `#e5e5e5`
|
||||||
|
- Radius: 16px
|
||||||
|
- Padding: 16px
|
||||||
|
- Hover: lift 2px, shadow `0 4px 0 #d7d7d7`
|
||||||
|
|
||||||
|
### Skill Tree Node (Lesson Bubble)
|
||||||
|
- Size: 80×72px
|
||||||
|
- Background: skill-color tinted (green for active, gray for locked)
|
||||||
|
- Border-bottom: 6px solid darker variant
|
||||||
|
- Radius: 50% (circular)
|
||||||
|
- Active: pulses 1.0 → 1.05 every 1.6s
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Border: 2px solid `#e5e5e5`
|
||||||
|
- Radius: 12px
|
||||||
|
- Padding: 12px 16px
|
||||||
|
- Focus: border `#1cb0f6` (eel blue), ring `0 0 0 3px rgba(28, 176, 246, 0.2)`
|
||||||
|
|
||||||
|
### Progress Bar
|
||||||
|
- Track: `#e5e5e5`
|
||||||
|
- Fill: `#58cc02` (or `#ff9600` for streak)
|
||||||
|
- Radius: 9999px
|
||||||
|
- Height: 16px
|
||||||
|
- Animated fill: 320ms ease-out on increment.
|
||||||
|
|
||||||
|
## 5. Spacing & Layout
|
||||||
|
|
||||||
|
- **Base unit**: 4px. Scale: 4, 8, 12, 16, 24, 32, 48, 64.
|
||||||
|
- **Container**: max 1080px, 24px gutter.
|
||||||
|
- **Lesson tree column**: 320px wide; centered on desktop.
|
||||||
|
|
||||||
|
## 6. Motion
|
||||||
|
|
||||||
|
- **Duration**: 180ms for button press; 320ms for skill-node unlock; 1.6s for active-node pulse.
|
||||||
|
- **Easing**: `cubic-bezier(0.34, 1.56, 0.64, 1)` (back-out, slight overshoot) for unlocks.
|
||||||
|
- **Mascot**: Duo blinks every 4–6s, jumps on streak milestones (480ms ease-out spring).
|
||||||
155
design-systems/github/DESIGN.md
Normal file
155
design-systems/github/DESIGN.md
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
# Design System Inspired by GitHub
|
||||||
|
|
||||||
|
> Category: Developer Tools
|
||||||
|
> Code-forward platform. Functional density, blue-on-white precision, Primer foundations.
|
||||||
|
|
||||||
|
## 1. Visual Theme & Atmosphere
|
||||||
|
|
||||||
|
GitHub's surface is engineered, not decorated. Every pixel announces a stance: this is a tool for people who care about diffs, builds, and pull requests. The page background is a clean `#ffffff` (light) or `#0d1117` (dark), with content arranged on dense rectangular panes separated by hairline borders rather than negative space. Information density is the brand — list rows, code lines, repository headers, and notification cards are all packed close together so a power user can scan a hundred items without scrolling.
|
||||||
|
|
||||||
|
The signature accents are the **Primer blue** (`#0969da`) for links and primary actions, and **GitHub green** (`#1a7f37`) for merged states, success, and the merge button itself. Both feel slightly muted compared to consumer-product blues and greens — saturated enough to read against the dense gray text, restrained enough to disappear into the background when several appear in one viewport.
|
||||||
|
|
||||||
|
Typography uses the **system-ui** stack across the entire product so text renders crisply on every OS, paired with **SFMono / Menlo / Consolas** for code. There is no editorial display font; GitHub's voice is the voice of the system you're already on.
|
||||||
|
|
||||||
|
**Key Characteristics:**
|
||||||
|
- True white canvas (`#ffffff`) or deep navy-black (`#0d1117`) — no warmth, no tint
|
||||||
|
- Hairline gray borders (`#d0d7de`) define every pane and panel
|
||||||
|
- Primer blue (`#0969da`) for links/primary; GitHub green (`#1a7f37`) for success/merge
|
||||||
|
- system-ui for prose; SFMono for code — no custom typeface
|
||||||
|
- Dense list rows with minimal padding; whitespace is rare
|
||||||
|
- Octicon iconography at 16px / 24px — single-stroke, geometric, consistent
|
||||||
|
- Pill-shaped status badges with strong color semantics
|
||||||
|
|
||||||
|
## 2. Color Palette & Roles
|
||||||
|
|
||||||
|
### Primary
|
||||||
|
- **Canvas Default** (`#ffffff`): Primary page background, light theme.
|
||||||
|
- **Canvas Subtle** (`#f6f8fa`): Secondary surface, sidebar, input background, header strip.
|
||||||
|
- **Canvas Inset** (`#eaeef2`): Code block background, deep-inset surface.
|
||||||
|
- **Fg Default** (`#1f2328`): Primary text, headlines, ink.
|
||||||
|
- **Fg Muted** (`#656d76`): Secondary text, captions, file paths.
|
||||||
|
|
||||||
|
### Brand Accent
|
||||||
|
- **Primer Blue** (`#0969da`): Links, primary CTAs, focus ring base — the universal interactive color.
|
||||||
|
- **Primer Blue Hover** (`#0550ae`): Hover/pressed for primary blue.
|
||||||
|
- **Accent Subtle** (`#ddf4ff`): Soft blue surface for callouts, info banners.
|
||||||
|
|
||||||
|
### Semantic
|
||||||
|
- **Success / Merge Green** (`#1a7f37`): Merged PRs, success badges, merge button.
|
||||||
|
- **Success Subtle** (`#dafbe1`): Success surface tint.
|
||||||
|
- **Open Green** (`#1a7f37`): "Open" issue/PR state.
|
||||||
|
- **Closed / Danger Red** (`#cf222e`): Closed PRs, destructive action, validation error.
|
||||||
|
- **Danger Subtle** (`#ffebe9`): Error banner surface.
|
||||||
|
- **Attention / Warning Yellow** (`#9a6700`): Warning text on amber surface.
|
||||||
|
- **Attention Subtle** (`#fff8c5`): Warning banner surface.
|
||||||
|
- **Done Purple** (`#8250df`): Merged-and-archived, "done" state, premium badge.
|
||||||
|
- **Sponsor Pink** (`#bf3989`): Sponsors heart, GitHub sponsors brand.
|
||||||
|
|
||||||
|
### Border & Divider
|
||||||
|
- **Border Default** (`#d0d7de`): Standard hairline border, panel outline.
|
||||||
|
- **Border Muted** (`#d8dee4`): Inner dividers within a panel.
|
||||||
|
- **Border Subtle** (`#eaeef2`): Faint table row dividers.
|
||||||
|
|
||||||
|
### Dark Theme
|
||||||
|
- **Dark Canvas** (`#0d1117`): Dark page background.
|
||||||
|
- **Dark Surface** (`#161b22`): Sidebar, header, secondary surface.
|
||||||
|
- **Dark Border** (`#30363d`): Standard dark-mode border.
|
||||||
|
- **Dark Fg** (`#e6edf3`): Primary text on dark.
|
||||||
|
|
||||||
|
## 3. Typography Rules
|
||||||
|
|
||||||
|
### Font Family
|
||||||
|
- **Body / UI**: `-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif`
|
||||||
|
- **Code / Mono**: `ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace`
|
||||||
|
- **Emoji**: `"Apple Color Emoji", "Segoe UI Emoji"`
|
||||||
|
|
||||||
|
### Hierarchy
|
||||||
|
|
||||||
|
| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |
|
||||||
|
|------|------|------|--------|-------------|----------------|-------|
|
||||||
|
| Display | system-ui | 32px (2rem) | 600 | 1.25 | -0.01em | Repo header, marketing hero |
|
||||||
|
| H1 | system-ui | 24px (1.5rem) | 600 | 1.25 | normal | Page heading |
|
||||||
|
| H2 | system-ui | 20px (1.25rem) | 600 | 1.25 | normal | Section heading |
|
||||||
|
| H3 | system-ui | 16px (1rem) | 600 | 1.25 | normal | Sub-section, panel header |
|
||||||
|
| Body | system-ui | 14px (0.875rem) | 400 | 1.5 | normal | Default text size — not 16px |
|
||||||
|
| Body Small | system-ui | 12px (0.75rem) | 400 | 1.4 | normal | Captions, file metadata |
|
||||||
|
| Code | SFMono | 12px (0.75rem) | 400 | 1.45 | normal | Code blocks, diff |
|
||||||
|
| Code Inline | SFMono | 0.85em | 400 | inherit | normal | Inline `code` spans |
|
||||||
|
|
||||||
|
### Principles
|
||||||
|
- **14px body, not 16px**: GitHub's prose density is its identity. The product reads at 14px to fit more rows in a viewport.
|
||||||
|
- **Weight binary**: 400 for everything by default; 600 for headlines and emphasis. No 500, no 700.
|
||||||
|
- **System fonts always**: never load a webfont for chrome — text must render instantly on slow connections.
|
||||||
|
|
||||||
|
## 4. Component Stylings
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
**Primary (Green)**
|
||||||
|
- Background: `#1f883d`
|
||||||
|
- Text: `#ffffff`
|
||||||
|
- Border: 1px solid `rgba(31, 35, 40, 0.15)`
|
||||||
|
- Padding: 5px 16px
|
||||||
|
- Radius: 6px
|
||||||
|
- Shadow: `0 1px 0 rgba(31,35,40,0.1)`
|
||||||
|
- Hover: background `#1a7f37`
|
||||||
|
- Use: "Create repository", "Merge pull request"
|
||||||
|
|
||||||
|
**Default**
|
||||||
|
- Background: `#f6f8fa`
|
||||||
|
- Text: `#1f2328`
|
||||||
|
- Border: 1px solid `#d0d7de`
|
||||||
|
- Padding: 5px 16px
|
||||||
|
- Radius: 6px
|
||||||
|
- Hover: background `#f3f4f6`, border `#d0d7de`
|
||||||
|
|
||||||
|
**Outline (Blue Link Style)**
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Text: `#0969da`
|
||||||
|
- Border: 1px solid `#d0d7de`
|
||||||
|
- Hover: background `#0969da`, text `#ffffff`
|
||||||
|
|
||||||
|
**Danger**
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Text: `#cf222e`
|
||||||
|
- Border: 1px solid `#d0d7de`
|
||||||
|
- Hover: background `#a40e26`, text `#ffffff`, border `#a40e26`
|
||||||
|
|
||||||
|
### Cards / Boxes
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Border: 1px solid `#d0d7de`
|
||||||
|
- Radius: 6px
|
||||||
|
- Padding: 16px (header) + 16px (body)
|
||||||
|
- Header has a `#f6f8fa` strip with bottom border.
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Border: 1px solid `#d0d7de`
|
||||||
|
- Radius: 6px
|
||||||
|
- Padding: 5px 12px
|
||||||
|
- Focus: border `#0969da`, ring `0 0 0 3px rgba(9,105,218,0.3)`
|
||||||
|
|
||||||
|
### Status Pills (Issue / PR)
|
||||||
|
- **Open**: background `#1a7f37`, text white, padding 4px 10px, radius 9999px.
|
||||||
|
- **Closed**: background `#cf222e`, text white.
|
||||||
|
- **Merged**: background `#8250df`, text white.
|
||||||
|
- **Draft**: background `#6e7781`, text white.
|
||||||
|
|
||||||
|
### Labels (Tags on Issues/PRs)
|
||||||
|
- Padding: 0 7px
|
||||||
|
- Radius: 9999px
|
||||||
|
- Font: 12px / 500
|
||||||
|
- Background and text are programmatic (label color → text computed for contrast).
|
||||||
|
|
||||||
|
## 5. Spacing & Layout
|
||||||
|
|
||||||
|
- **Base unit**: 4px. Spacing scale: 4, 8, 12, 16, 24, 32, 40, 48.
|
||||||
|
- **Page max-width**: 1280px (`Container-xl`).
|
||||||
|
- **Sidebar**: 296px on desktop, collapses below 1012px.
|
||||||
|
- **Row padding**: 16px horizontal, 12px vertical (lists are dense by design).
|
||||||
|
|
||||||
|
## 6. Motion
|
||||||
|
|
||||||
|
- **Duration**: 80ms for hover; 200ms for menu/popover open.
|
||||||
|
- **Easing**: `ease-out` for opens, `ease-in` for closes.
|
||||||
|
- **Avoided**: page-load animation, parallax, persistent micro-interactions. Things appear; they do not perform.
|
||||||
149
design-systems/huggingface/DESIGN.md
Normal file
149
design-systems/huggingface/DESIGN.md
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
# Design System Inspired by Hugging Face
|
||||||
|
|
||||||
|
> Category: AI & LLM
|
||||||
|
> ML community hub. Sunny yellow accent, monospace identity, cheerful and dense.
|
||||||
|
|
||||||
|
## 1. Visual Theme & Atmosphere
|
||||||
|
|
||||||
|
Hugging Face is the rare ML brand that refuses to look serious. The hub leans into a sunshine-yellow accent (`#ffd21e`), a cartoon hugging-face emoji as the logo, and a confident **IBM Plex Mono** voice that reads more like a community zine than a research lab. The page background is a clean off-white (`#fafafa`) with text in a deep slate (`#0d1117`), and the yellow appears in pull quotes, tags, "new" badges, and the model-card header strip — never as an entire surface, always as punctuation.
|
||||||
|
|
||||||
|
The typographic system is monospace-forward in a way few product brands attempt: **IBM Plex Mono** for headings and tags, **Source Sans Pro** (or Inter) for body. The mix gives every page a "config file is the README" vibe — fitting for a platform built around `.gitattributes` and `model-card.md`.
|
||||||
|
|
||||||
|
Shapes are crisp, not soft: 4–6px radii, 1px solid borders that announce themselves rather than hide. Tables are dense, with row hover in a faint gray (`#f3f4f6`). The brand emoji punctuates everything — chips, headings, even error states wear a 🤗 — so the system feels human even when displaying technical data.
|
||||||
|
|
||||||
|
**Key Characteristics:**
|
||||||
|
- Sunshine yellow `#ffd21e` as the lone saturated accent
|
||||||
|
- IBM Plex Mono for headings and tags; Source Sans Pro for body
|
||||||
|
- Off-white canvas (`#fafafa`) with crisp 1px borders (`#e5e7eb`)
|
||||||
|
- 4–6px radii — closer to brutalist than rounded
|
||||||
|
- Hugging-face emoji 🤗 used unironically as a system glyph
|
||||||
|
- Dense tables, minimal padding — a community hub for power users
|
||||||
|
- Color-coded model categories (NLP blue, vision green, audio purple)
|
||||||
|
|
||||||
|
## 2. Color Palette & Roles
|
||||||
|
|
||||||
|
### Primary
|
||||||
|
- **HF Yellow** (`#ffd21e`): Brand primary, badges, "new" pill, model-card header bar.
|
||||||
|
- **HF Yellow Deep** (`#f59e0b`): Hover/active for yellow.
|
||||||
|
- **HF Yellow Soft** (`#fff4cc`): Surface tint, callout background.
|
||||||
|
|
||||||
|
### Surface & Background
|
||||||
|
- **Canvas** (`#ffffff`): Primary page background.
|
||||||
|
- **Canvas Subtle** (`#fafafa`): Alternate section background, footer.
|
||||||
|
- **Canvas Inset** (`#f3f4f6`): Code block background, hover row.
|
||||||
|
- **Canvas Dark** (`#0d1117`): Dark theme background.
|
||||||
|
|
||||||
|
### Ink & Text
|
||||||
|
- **Ink Primary** (`#0d1117`): Primary text, headings.
|
||||||
|
- **Ink Secondary** (`#374151`): Body prose.
|
||||||
|
- **Ink Muted** (`#6b7280`): Captions, file paths, model authors.
|
||||||
|
- **Ink Inverse** (`#f9fafb`): Text on dark surface.
|
||||||
|
|
||||||
|
### Category Accents (Model Tasks)
|
||||||
|
- **NLP Blue** (`#2563eb`): Text/NLP task badges.
|
||||||
|
- **Vision Green** (`#16a34a`): Computer-vision task badges.
|
||||||
|
- **Audio Purple** (`#9333ea`): Audio/speech task badges.
|
||||||
|
- **Multimodal Pink** (`#db2777`): Multimodal/diffusion task badges.
|
||||||
|
- **Tabular Orange** (`#ea580c`): Tabular/structured task badges.
|
||||||
|
|
||||||
|
### Semantic
|
||||||
|
- **Success** (`#16a34a`): Build succeeded, deploy live.
|
||||||
|
- **Warning** (`#f59e0b`): Slow inference, rate limit.
|
||||||
|
- **Error** (`#dc2626`): Failed build, broken model.
|
||||||
|
- **Info** (`#2563eb`): Notice banner.
|
||||||
|
|
||||||
|
### Border
|
||||||
|
- **Border Default** (`#e5e7eb`): Standard 1px hairline.
|
||||||
|
- **Border Strong** (`#d1d5db`): Emphasized border on hover.
|
||||||
|
- **Border Subtle** (`#f3f4f6`): Inner divider.
|
||||||
|
|
||||||
|
## 3. Typography Rules
|
||||||
|
|
||||||
|
### Font Family
|
||||||
|
- **Display / UI / Headings / Tags**: `IBM Plex Mono`, with fallback: `ui-monospace, SFMono-Regular, Menlo, Consolas, monospace`
|
||||||
|
- **Body / Prose**: `Source Sans Pro`, with fallback: `Inter, system-ui, -apple-system, sans-serif`
|
||||||
|
- **Editorial (rare, blog only)**: `Source Serif Pro`, with fallback: `Georgia, serif`
|
||||||
|
|
||||||
|
### Hierarchy
|
||||||
|
|
||||||
|
| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |
|
||||||
|
|------|------|------|--------|-------------|----------------|-------|
|
||||||
|
| Display | IBM Plex Mono | 48px (3rem) | 600 | 1.1 | -0.01em | Marketing hero |
|
||||||
|
| H1 | IBM Plex Mono | 32px (2rem) | 600 | 1.2 | normal | Page heading, model name |
|
||||||
|
| H2 | IBM Plex Mono | 24px (1.5rem) | 600 | 1.25 | normal | Section heading |
|
||||||
|
| H3 | IBM Plex Mono | 18px (1.125rem) | 600 | 1.3 | normal | Sub-section |
|
||||||
|
| Body Large | Source Sans Pro | 18px (1.125rem) | 400 | 1.6 | normal | Lede, blog intro |
|
||||||
|
| Body | Source Sans Pro | 15px (0.9375rem) | 400 | 1.55 | normal | Standard prose, model card |
|
||||||
|
| Caption | Source Sans Pro | 13px (0.8125rem) | 500 | 1.4 | 0.01em | Author byline, timestamp |
|
||||||
|
| Tag / Badge | IBM Plex Mono | 12px (0.75rem) | 500 | 1.2 | 0.02em | Task tags, framework chips |
|
||||||
|
| Code | IBM Plex Mono | 14px (0.875rem) | 400 | 1.55 | normal | Code blocks, inline `model_id` |
|
||||||
|
|
||||||
|
### Principles
|
||||||
|
- **Mono everywhere it matters**: nav links, headings, tags, and metadata are all monospaced. Sans is reserved for paragraphs of prose.
|
||||||
|
- **Weight under 600**: 600 is the cap; 700 is too loud against yellow. Hierarchy is size and color.
|
||||||
|
- **Tags read as code**: model tags use mono so they look like the strings developers will paste into Python.
|
||||||
|
|
||||||
|
## 4. Component Stylings
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
**Primary**
|
||||||
|
- Background: `#0d1117`
|
||||||
|
- Text: `#ffffff`
|
||||||
|
- Padding: 8px 16px
|
||||||
|
- Radius: 6px
|
||||||
|
- Hover: `#374151`
|
||||||
|
- Use: "Use this model", primary forms.
|
||||||
|
|
||||||
|
**Yellow CTA**
|
||||||
|
- Background: `#ffd21e`
|
||||||
|
- Text: `#0d1117`
|
||||||
|
- Padding: 8px 16px
|
||||||
|
- Radius: 6px
|
||||||
|
- Hover: `#f59e0b`
|
||||||
|
- Use: "Pro upgrade", "Sponsor".
|
||||||
|
|
||||||
|
**Outline**
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Text: `#0d1117`
|
||||||
|
- Border: 1px solid `#e5e7eb`
|
||||||
|
- Hover: background `#f3f4f6`
|
||||||
|
|
||||||
|
### Cards / Model Cards
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Border: 1px solid `#e5e7eb`
|
||||||
|
- Radius: 6px
|
||||||
|
- Padding: 16px 20px
|
||||||
|
- Header strip: `#ffd21e` background, 4px tall, only on featured model cards.
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Border: 1px solid `#e5e7eb`
|
||||||
|
- Radius: 6px
|
||||||
|
- Padding: 8px 12px
|
||||||
|
- Focus: border `#0d1117`, ring `0 0 0 3px rgba(13,17,23,0.1)`
|
||||||
|
|
||||||
|
### Tags / Chips (Task / Framework)
|
||||||
|
- Background: category-tinted soft (`#dbeafe` for NLP, `#dcfce7` for vision, etc.)
|
||||||
|
- Text: matching strong category color.
|
||||||
|
- Padding: 2px 8px
|
||||||
|
- Radius: 4px
|
||||||
|
- Font: IBM Plex Mono 12px / 500
|
||||||
|
|
||||||
|
### Tables
|
||||||
|
- Header: background `#fafafa`, border-bottom 1px `#e5e7eb`.
|
||||||
|
- Row: border-bottom 1px `#f3f4f6`, hover `#f3f4f6`.
|
||||||
|
- Padding: 8px 16px per cell — dense by design.
|
||||||
|
|
||||||
|
## 5. Spacing & Layout
|
||||||
|
|
||||||
|
- **Base unit**: 4px. Scale: 4, 8, 12, 16, 24, 32, 48, 64.
|
||||||
|
- **Container**: max 1280px, 24px gutter.
|
||||||
|
- **Sidebar (model browser)**: 280px wide.
|
||||||
|
- **Section rhythm**: 64–96px vertical between major sections.
|
||||||
|
|
||||||
|
## 6. Motion
|
||||||
|
|
||||||
|
- **Duration**: 120ms for hover; 200ms for menu open.
|
||||||
|
- **Easing**: `ease-out`.
|
||||||
|
- **Tag pop**: a 1.05× scale on hover at 120ms — the only exception to flat-on-hover.
|
||||||
140
design-systems/openai/DESIGN.md
Normal file
140
design-systems/openai/DESIGN.md
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
# Design System Inspired by OpenAI
|
||||||
|
|
||||||
|
> Category: AI & LLM
|
||||||
|
> Calm, near-monochrome system anchored in deep teal-black with generous white space and editorial typography.
|
||||||
|
|
||||||
|
## 1. Visual Theme & Atmosphere
|
||||||
|
|
||||||
|
OpenAI's product surface reads like a research lab dressed for the public — clinical, restrained, deliberately quiet. The page background is a true white (`#ffffff`) layered against a near-black ink (`#0d0d0d`) with a subtle teal undertone, so even the text feels slightly cooled rather than aggressively dark. The result is a chromatic neutrality that puts model output, code, and prose front and center, not the chrome around them.
|
||||||
|
|
||||||
|
The signature move is the use of **Söhne** (or its system stand-in `inter`) at restrained weights — 400 for body, 500 for nav and labels, 600 for emphasis — paired with **Signifier**, a contemporary serif used for editorial display. Where most AI brands lean futuristic, OpenAI's serif headlines give the product a quietly literary tone, as if every announcement is an essay.
|
||||||
|
|
||||||
|
The shape system is uniformly soft: 8px–12px radii, 9999px pills for tags and chips, no harsh corners anywhere. Section transitions are denoted by whitespace rather than dividers; when borders appear they are `#e5e5e5` hairlines that read as the absence of color rather than its presence.
|
||||||
|
|
||||||
|
**Key Characteristics:**
|
||||||
|
- True white canvas (`#ffffff`) with deep teal-black ink (`#0d0d0d`)
|
||||||
|
- Söhne / Inter at modest weights (400, 500, 600) — restraint over assertion
|
||||||
|
- Signifier serif for editorial display headlines
|
||||||
|
- Soft 8–12px radii everywhere; 9999px pills for chips
|
||||||
|
- Hairline borders (`#e5e5e5`) used sparingly; whitespace as primary divider
|
||||||
|
- Single-color illustrations in deep teal — no gradients in marks
|
||||||
|
- Generous line-height (1.55–1.65) and tracking near zero
|
||||||
|
|
||||||
|
## 2. Color Palette & Roles
|
||||||
|
|
||||||
|
### Primary
|
||||||
|
- **Pure White** (`#ffffff`): Primary background, card surface, button background.
|
||||||
|
- **Ink Black** (`#0d0d0d`): Primary text, brand mark, primary CTA.
|
||||||
|
- **Soft Black** (`#1a1a1a`): Secondary heading, alternative ink for non-critical text.
|
||||||
|
|
||||||
|
### Surface & Background
|
||||||
|
- **Mist** (`#fafafa`): Section break background, footer surface.
|
||||||
|
- **Pearl** (`#f5f5f5`): Card surface, elevated panel.
|
||||||
|
- **Cloud** (`#ececec`): Disabled background, divider tint.
|
||||||
|
|
||||||
|
### Brand Accent
|
||||||
|
- **OpenAI Teal** (`#10a37f`): Brand primary, link, highlight badge — the lone color in an otherwise neutral system.
|
||||||
|
- **Teal Deep** (`#0a7a5e`): Hover and pressed state for the brand color.
|
||||||
|
- **Teal Soft** (`#e8f5f0`): Surface tint for success badges, highlight callouts.
|
||||||
|
|
||||||
|
### Neutrals & Text
|
||||||
|
- **Graphite** (`#3c3c3c`): Body text, default reading color.
|
||||||
|
- **Slate** (`#6e6e6e`): Secondary text, captions, metadata.
|
||||||
|
- **Ash** (`#9b9b9b`): Tertiary text, placeholder, disabled label.
|
||||||
|
- **Stone** (`#c4c4c4`): Decorative dividers, faint icons.
|
||||||
|
|
||||||
|
### Semantic & Border
|
||||||
|
- **Border Hairline** (`#e5e5e5`): Standard hairline separator.
|
||||||
|
- **Border Soft** (`#ededed`): Card outline on white surface.
|
||||||
|
- **Error** (`#ef4146`): Validation, destructive action.
|
||||||
|
- **Warning** (`#f5a623`): Soft amber for advisory states.
|
||||||
|
- **Info** (`#2563eb`): Informational link tone (used sparingly; teal still wins).
|
||||||
|
|
||||||
|
## 3. Typography Rules
|
||||||
|
|
||||||
|
### Font Family
|
||||||
|
- **Display / Editorial**: `Signifier`, with fallback: `'Source Serif Pro', Georgia, serif`
|
||||||
|
- **Body / UI**: `Söhne`, with fallback: `Inter, system-ui, -apple-system, 'Segoe UI', sans-serif`
|
||||||
|
- **Code / Mono**: `Söhne Mono`, with fallback: `ui-monospace, 'JetBrains Mono', Menlo, Consolas, monospace`
|
||||||
|
|
||||||
|
### Hierarchy
|
||||||
|
|
||||||
|
| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |
|
||||||
|
|------|------|------|--------|-------------|----------------|-------|
|
||||||
|
| Display | Signifier | 56px (3.5rem) | 400 | 1.08 | -0.02em | Editorial hero, announcement titles |
|
||||||
|
| H1 | Söhne | 40px (2.5rem) | 600 | 1.15 | -0.01em | Page heading |
|
||||||
|
| H2 | Söhne | 28px (1.75rem) | 600 | 1.2 | -0.005em | Section heading |
|
||||||
|
| H3 | Söhne | 20px (1.25rem) | 600 | 1.3 | normal | Sub-section |
|
||||||
|
| Body Large | Söhne | 18px (1.125rem) | 400 | 1.6 | normal | Lede paragraphs |
|
||||||
|
| Body | Söhne | 16px (1rem) | 400 | 1.65 | normal | Standard reading text |
|
||||||
|
| Body Small | Söhne | 14px (0.875rem) | 400 | 1.55 | normal | Card body, dense UI |
|
||||||
|
| Caption | Söhne | 13px (0.8125rem) | 500 | 1.4 | 0.01em | Metadata, badges |
|
||||||
|
| Label | Söhne | 12px (0.75rem) | 500 | 1.3 | 0.04em | Eyebrow, uppercase nav links |
|
||||||
|
| Code | Söhne Mono | 14px (0.875rem) | 400 | 1.55 | normal | Code blocks, terminal output |
|
||||||
|
|
||||||
|
### Principles
|
||||||
|
- **Restraint as identity**: weights cap at 600; 700+ feels off-brand. Hierarchy comes from size and color, not weight.
|
||||||
|
- **Serif for soul, sans for system**: Signifier appears only in editorial display moments. The product UI is sans-only.
|
||||||
|
- **Negative tracking on display**: -0.02em on display sizes; tracking returns to zero by 16px.
|
||||||
|
|
||||||
|
## 4. Component Stylings
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
|
||||||
|
**Primary**
|
||||||
|
- Background: `#0d0d0d`
|
||||||
|
- Text: `#ffffff`
|
||||||
|
- Padding: 10px 18px
|
||||||
|
- Radius: 9999px (full pill) on chips, 12px on rectangular CTAs
|
||||||
|
- Hover: `#1a1a1a` background
|
||||||
|
- Use: Primary CTA, "Try ChatGPT", "Sign in"
|
||||||
|
|
||||||
|
**Secondary**
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Text: `#0d0d0d`
|
||||||
|
- Border: 1px solid `#e5e5e5`
|
||||||
|
- Padding: 10px 18px
|
||||||
|
- Radius: 12px
|
||||||
|
- Hover: background `#fafafa`, border `#d4d4d4`
|
||||||
|
|
||||||
|
**Brand Accent**
|
||||||
|
- Background: `#10a37f`
|
||||||
|
- Text: `#ffffff`
|
||||||
|
- Padding: 10px 18px
|
||||||
|
- Radius: 12px
|
||||||
|
- Hover: `#0a7a5e`
|
||||||
|
- Use: Highlighted upgrade CTA, success path
|
||||||
|
|
||||||
|
### Cards
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Border: 1px solid `#ededed`
|
||||||
|
- Radius: 16px
|
||||||
|
- Padding: 24px–32px
|
||||||
|
- Shadow: none by default; on hover `0 4px 16px rgba(13,13,13,0.06)`
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- Background: `#ffffff`
|
||||||
|
- Border: 1px solid `#e5e5e5`
|
||||||
|
- Radius: 12px
|
||||||
|
- Padding: 12px 14px
|
||||||
|
- Focus: border `#10a37f`, ring `0 0 0 3px rgba(16,163,127,0.12)`
|
||||||
|
|
||||||
|
### Pills & Tags
|
||||||
|
- Background: `#f5f5f5`
|
||||||
|
- Text: `#3c3c3c`
|
||||||
|
- Padding: 4px 10px
|
||||||
|
- Radius: 9999px
|
||||||
|
- Font: 12px / 500
|
||||||
|
|
||||||
|
## 5. Spacing & Layout
|
||||||
|
|
||||||
|
- **Base unit**: 4px. Scale: 4, 8, 12, 16, 24, 32, 48, 64, 96, 128.
|
||||||
|
- **Container**: max-width 1200px, 24px gutter on mobile, 48px on desktop.
|
||||||
|
- **Section rhythm**: 96–128px vertical between major sections; 64px on mobile.
|
||||||
|
- **Grid**: 12-column desktop, 4-column mobile, 24px gap.
|
||||||
|
|
||||||
|
## 6. Motion
|
||||||
|
|
||||||
|
- **Duration**: 150–220ms for hover; 280–360ms for layout transitions.
|
||||||
|
- **Easing**: `cubic-bezier(0.16, 1, 0.3, 1)` (smooth out) for entrances.
|
||||||
|
- **Restraint**: no parallax, no scroll-jacking. Subtle fade and translate only.
|
||||||
Loading…
Reference in a new issue