mirror of
https://github.com/hanzoai/o11y-components.git
synced 2026-10-30 14:54:15 +00:00
A version written into a manifest is written into every manifest that wants it, which is how eslint came to exist in a hundred and four forms across this estate and how a package came to resolve twice at run time. The catalog moves the number out of the manifest: a manifest names the package, the workspace names the version. Four are held below their latest because the newer release cannot be used, and the reason is recorded rather than the version alone: @noble/hashes 2 renamed every subpath the web3 stack imports, @splinetool/runtime 2 depends on a package that was never published, @dynamic-labs 5.4.1 the same, and react-resizable-panels 3 points its types condition at the bundle.
364 lines
9.8 KiB
TypeScript
364 lines
9.8 KiB
TypeScript
import { colorTokens, semanticTokens } from '@signozhq/design-tokens';
|
|
import { Button } from '@signozhq/ui';
|
|
import { useMemo, useState } from 'react';
|
|
import type { TokenData } from './TokenRow.js';
|
|
import { TokenSearch } from './TokenSearch.js';
|
|
import { TokenTable } from './TokenTable.js';
|
|
|
|
type ThemeMode = 'light' | 'dark';
|
|
|
|
export type SemanticTokenGroup =
|
|
| 'surface-levels'
|
|
| 'actions'
|
|
| 'callouts'
|
|
| 'accents'
|
|
| 'sidebar'
|
|
| 'charts'
|
|
| 'fonts'
|
|
| 'base'
|
|
| 'alerts'
|
|
| 'legacy';
|
|
|
|
export interface TokenReferenceProps {
|
|
groups?: SemanticTokenGroup[];
|
|
mode?: 'semantic' | 'primitive' | 'all';
|
|
showThemeToggle?: boolean;
|
|
showSearch?: boolean;
|
|
showCategoryFilter?: boolean;
|
|
showModeTabs?: boolean;
|
|
showHeader?: boolean;
|
|
title?: string;
|
|
description?: string;
|
|
}
|
|
|
|
interface TokenWithMetadata {
|
|
value: string;
|
|
description?: string;
|
|
usage?: string;
|
|
dontUse?: string;
|
|
category?: string;
|
|
group?: string;
|
|
}
|
|
|
|
type TokenValue = string | TokenWithMetadata;
|
|
|
|
function getTokenValue(token: TokenValue): string {
|
|
return typeof token === 'string' ? token : token.value;
|
|
}
|
|
|
|
function getTokenMetadata(token: TokenValue): Omit<TokenWithMetadata, 'value'> {
|
|
if (typeof token === 'string') {
|
|
return {};
|
|
}
|
|
const { value: _, ...metadata } = token;
|
|
return metadata;
|
|
}
|
|
|
|
function parseSemanticTokens(mode: ThemeMode): TokenData[] {
|
|
const themeTokens = semanticTokens.default?.[mode]?.tokens;
|
|
if (!themeTokens) return [];
|
|
|
|
return Object.entries(themeTokens).map(([name, token]) => ({
|
|
name,
|
|
value: getTokenValue(token as TokenValue),
|
|
...getTokenMetadata(token as TokenValue),
|
|
}));
|
|
}
|
|
|
|
interface ColorShade {
|
|
value: string;
|
|
type: string;
|
|
description?: string;
|
|
}
|
|
|
|
function parsePrimitiveColors(): TokenData[] {
|
|
const tokens: TokenData[] = [];
|
|
const bg = colorTokens.bg as Record<string, Record<string, ColorShade>>;
|
|
|
|
for (const [colorName, shades] of Object.entries(bg)) {
|
|
for (const [shadeName, shadeData] of Object.entries(shades)) {
|
|
tokens.push({
|
|
name: `bg-${colorName.toLowerCase()}-${shadeName}`,
|
|
value: shadeData.value,
|
|
description: shadeData.description || `${colorName} ${shadeName}`,
|
|
category: 'background',
|
|
group: colorName.toLowerCase(),
|
|
});
|
|
}
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
|
|
const GROUPS_ORDER: SemanticTokenGroup[] = [
|
|
'surface-levels',
|
|
'actions',
|
|
'callouts',
|
|
'accents',
|
|
'sidebar',
|
|
'charts',
|
|
'fonts',
|
|
'base',
|
|
'alerts',
|
|
'legacy',
|
|
];
|
|
|
|
function groupTokensByGroup(tokens: TokenData[]): Map<string, TokenData[]> {
|
|
const grouped = new Map<string, TokenData[]>();
|
|
|
|
for (const token of tokens) {
|
|
const group = token.group || 'other';
|
|
if (!grouped.has(group)) {
|
|
grouped.set(group, []);
|
|
}
|
|
grouped.get(group)!.push(token);
|
|
}
|
|
|
|
const sortedGroups = new Map<string, TokenData[]>();
|
|
for (const group of GROUPS_ORDER) {
|
|
if (grouped.has(group)) {
|
|
sortedGroups.set(group, grouped.get(group)!);
|
|
}
|
|
}
|
|
for (const [group, tokens] of grouped) {
|
|
if (!sortedGroups.has(group)) {
|
|
sortedGroups.set(group, tokens);
|
|
}
|
|
}
|
|
|
|
return sortedGroups;
|
|
}
|
|
|
|
export const GROUP_LABELS: Record<string, string> = {
|
|
'surface-levels': 'Surface Levels (L1, L2, L3)',
|
|
actions: 'Action Colors (Primary, Secondary, Success, Warning, Danger)',
|
|
callouts: 'Callout Components',
|
|
accents: 'Accent Colors',
|
|
sidebar: 'Sidebar',
|
|
charts: 'Chart Colors',
|
|
fonts: 'Typography',
|
|
base: 'Base Colors',
|
|
alerts: 'Alerts',
|
|
legacy: 'Legacy Tokens',
|
|
};
|
|
|
|
export const GROUP_DESCRIPTIONS: Record<string, string> = {
|
|
'surface-levels':
|
|
'Hierarchical background, foreground, and border colors for layered UI surfaces.',
|
|
actions: 'Colors for interactive elements like buttons, links, and form controls.',
|
|
callouts: 'Styled containers for informational, success, warning, and error messages.',
|
|
accents: 'Brand and semantic accent colors for highlights and data visualization.',
|
|
sidebar: 'Tokens specific to sidebar navigation components.',
|
|
charts: 'Color palette optimized for data visualization and charts.',
|
|
fonts: 'Typography font family definitions.',
|
|
base: 'Fundamental black and white base colors.',
|
|
alerts: 'Alert strip and notification styling tokens.',
|
|
legacy: 'Deprecated tokens maintained for backward compatibility. Migrate to semantic tokens.',
|
|
};
|
|
|
|
export function TokenReference({
|
|
groups,
|
|
mode = 'all',
|
|
showThemeToggle = true,
|
|
showSearch = true,
|
|
showCategoryFilter = true,
|
|
showModeTabs = true,
|
|
showHeader = false,
|
|
title = 'Token Reference',
|
|
description = 'Design tokens for building consistent UI',
|
|
}: TokenReferenceProps) {
|
|
const [themeMode, setThemeMode] = useState<ThemeMode>('dark');
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
|
const [activeSection, setActiveSection] = useState<'semantic' | 'primitive'>(
|
|
mode === 'primitive' ? 'primitive' : 'semantic'
|
|
);
|
|
|
|
const semanticTokensData = useMemo(() => parseSemanticTokens(themeMode), [themeMode]);
|
|
|
|
const primitiveTokensData = useMemo(() => parsePrimitiveColors(), []);
|
|
|
|
const filteredByGroups = useMemo(() => {
|
|
if (!groups || groups.length === 0) {
|
|
return semanticTokensData;
|
|
}
|
|
return semanticTokensData.filter(
|
|
(token) => token.group && groups.includes(token.group as SemanticTokenGroup)
|
|
);
|
|
}, [semanticTokensData, groups]);
|
|
|
|
const categories = useMemo(() => {
|
|
const tokensToCheck = groups ? filteredByGroups : semanticTokensData;
|
|
const cats = new Set<string>();
|
|
for (const token of tokensToCheck) {
|
|
if (token.category) cats.add(token.category);
|
|
}
|
|
return Array.from(cats).sort();
|
|
}, [filteredByGroups, semanticTokensData, groups]);
|
|
|
|
const shouldShowSemantic = mode === 'all' || mode === 'semantic';
|
|
const shouldShowPrimitive = mode === 'all' || mode === 'primitive';
|
|
const effectiveActiveSection =
|
|
mode === 'semantic' ? 'semantic' : mode === 'primitive' ? 'primitive' : activeSection;
|
|
|
|
const filteredTokens = useMemo(() => {
|
|
const baseTokens =
|
|
effectiveActiveSection === 'semantic' ? filteredByGroups : primitiveTokensData;
|
|
|
|
return baseTokens.filter((token) => {
|
|
const matchesSearch =
|
|
!searchQuery ||
|
|
token.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
token.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
token.value.toLowerCase().includes(searchQuery.toLowerCase());
|
|
|
|
const matchesCategory = !selectedCategory || token.category === selectedCategory;
|
|
|
|
return matchesSearch && matchesCategory;
|
|
});
|
|
}, [
|
|
effectiveActiveSection,
|
|
filteredByGroups,
|
|
primitiveTokensData,
|
|
searchQuery,
|
|
selectedCategory,
|
|
]);
|
|
|
|
const groupedTokens = useMemo(() => groupTokensByGroup(filteredTokens), [filteredTokens]);
|
|
|
|
const showTabs = showModeTabs && mode === 'all';
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
minHeight: '100vh',
|
|
backgroundColor: 'var(--l1-background)',
|
|
padding: 24,
|
|
paddingTop: 0,
|
|
}}
|
|
>
|
|
<div className="stack-24" style={{ maxWidth: 1280, marginInline: 'auto' }}>
|
|
{showHeader && (
|
|
<div className="stack-to-row" style={{ gap: 16 }}>
|
|
<div>
|
|
<h1
|
|
style={{
|
|
fontSize: 24,
|
|
lineHeight: 1.33333,
|
|
fontWeight: 700,
|
|
color: 'var(--l1-foreground)',
|
|
}}
|
|
>
|
|
{title}
|
|
</h1>
|
|
<p
|
|
style={{
|
|
fontSize: 14,
|
|
lineHeight: 1.42857,
|
|
color: 'var(--l2-foreground)',
|
|
marginTop: 4,
|
|
}}
|
|
>
|
|
{description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{showTabs && (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
gap: 8,
|
|
borderBottomStyle: 'solid',
|
|
borderBottomWidth: 1,
|
|
borderColor: 'var(--l2-border)',
|
|
}}
|
|
>
|
|
{shouldShowSemantic && (
|
|
<Button
|
|
onClick={() => {
|
|
setActiveSection('semantic');
|
|
setSelectedCategory(null);
|
|
}}
|
|
variant={activeSection === 'semantic' ? 'solid' : 'link'}
|
|
color={activeSection === 'semantic' ? 'primary' : 'secondary'}
|
|
>
|
|
Semantic Tokens
|
|
</Button>
|
|
)}
|
|
{shouldShowPrimitive && (
|
|
<Button
|
|
type="button"
|
|
onClick={() => {
|
|
setActiveSection('primitive');
|
|
setSelectedCategory(null);
|
|
}}
|
|
variant={activeSection === 'primitive' ? 'solid' : 'ghost'}
|
|
color={activeSection === 'primitive' ? 'primary' : 'secondary'}
|
|
>
|
|
Primitive Colors
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{showSearch && (
|
|
<TokenSearch
|
|
onSearch={setSearchQuery}
|
|
onCategoryFilter={showCategoryFilter ? setSelectedCategory : undefined}
|
|
categories={showCategoryFilter ? categories : []}
|
|
selectedCategory={selectedCategory}
|
|
/>
|
|
)}
|
|
|
|
<div className="stack-to-row" style={{ gap: 12 }}>
|
|
<div style={{ fontSize: 14, lineHeight: 1.42857, color: 'var(--l3-foreground)' }}>
|
|
Showing {filteredTokens.length} tokens
|
|
{searchQuery && ` matching "${searchQuery}"`}
|
|
{selectedCategory && ` in ${selectedCategory}`}
|
|
</div>
|
|
|
|
{showThemeToggle && (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
borderRadius: 4,
|
|
borderStyle: 'solid',
|
|
borderWidth: 1,
|
|
borderColor: 'var(--l2-border)',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<Button
|
|
type="button"
|
|
onClick={() => setThemeMode('light')}
|
|
variant={themeMode === 'light' ? 'solid' : 'ghost'}
|
|
color={themeMode === 'light' ? 'primary' : 'secondary'}
|
|
>
|
|
Light
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
onClick={() => setThemeMode('dark')}
|
|
variant={themeMode === 'dark' ? 'solid' : 'ghost'}
|
|
color={themeMode === 'dark' ? 'primary' : 'secondary'}
|
|
>
|
|
Dark
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="stack-24">
|
|
{Array.from(groupedTokens.entries()).map(([group, tokens]) => (
|
|
<TokenTable key={group} tokens={tokens} title={GROUP_LABELS[group] || group} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|