import { tokens } from '@preply/ds-core'; import { Token } from '@preply/ds-core-types'; import ShareSvg from '@preply/ds-media-icons/dist/24/TokyoUIShare.svg'; import { useColorScheme, useTheme } from '@preply/ds-web-core'; // eslint-disable-next-line no-restricted-imports import { getToken } from '@preply/ds-web-core/dist/token/private/getToken'; import { Heading, IconButton, LayoutFlex, LayoutFlexItem, SelectField, SelectFieldOption, Text, TextField, ToastProvider, Tooltip, showToast, } from '@preply/ds-web-lib'; import React, { FC, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react'; import { TokenPreview } from './TokenPreview'; import { getFigmaTokenName } from './figmaTokens'; import { collectTokens } from './flattenTokens'; import { getGroupName, shouldShowSectionHeader, getTokenAlternativeSyntaxes, } from './sectionHeaders'; import { filterTokens } from './tokenFilter'; const URL_CHANGE_EVENT = 'token-explorer-url-change'; type CategoryOption = { label: string; value: string; category?: string; categories?: string[]; allowedTokens?: string[]; }; const CATEGORY_OPTIONS: CategoryOption[] = [ { label: 'Color', value: 'color', category: 'color' }, { label: 'Sizing', value: 'sizing', category: 'sizing' }, { label: 'Spacing', value: 'spacing', category: 'spacing' }, { label: 'Radius', value: 'radius', category: 'radius' }, { label: 'Action & Button', value: 'action-button', categories: ['action', 'button'] }, { label: 'Typography', value: 'typography', categories: ['text', 'heading', 'link'] }, { label: 'Elevation', value: 'elevation', category: 'dropShadow' }, { label: 'Exp colors (full palette)', value: 'exp', category: 'exp' }, { label: 'AI', value: 'ai', allowedTokens: [ 'color.background.ai.surface.linearGradient', 'color.border.ai.button.linearGradient', 'color.border.ai.surface.linearGradient', ], }, ]; function getUrlParam(key: string): string { try { return ( new URL(window.top?.location.href || window.location.href).searchParams.get(key) || '' ); } catch { return new URLSearchParams(window.location.search).get(key) || ''; } } function setUrlParam(key: string, value: string | null) { try { const url = new URL(window.top?.location.href || window.location.href); if (value === null || value === '') { url.searchParams.delete(key); } else { url.searchParams.set(key, value); } window.top?.history.replaceState({}, '', url); } catch { // ignore if cross-origin } window.dispatchEvent(new Event(URL_CHANGE_EVENT)); } function useUrlState(key: string): [string, (v: string) => void] { const subscribe = useCallback((cb: () => void) => { window.addEventListener(URL_CHANGE_EVENT, cb); return () => window.removeEventListener(URL_CHANGE_EVENT, cb); }, []); const getSnapshot = useCallback(() => getUrlParam(key), [key]); const value = useSyncExternalStore(subscribe, getSnapshot); const setValue = useCallback((v: string) => setUrlParam(key, v || null), [key]); return [value, setValue]; } export const TokenExplorer: FC<{ category?: string; categories?: string[]; filter?: string; groupBySegment?: number; allowedGroups?: string[]; allowedTokens?: string[]; }> = ({ category: categoryProp, categories: categoriesProp, filter: nameFilter, groupBySegment, allowedGroups, allowedTokens: allowedTokensProp, }) => { const { theme } = useTheme(); const { colorScheme } = useColorScheme(); const [, setSearchUrl] = useUrlState('filter'); const [searchInput, setSearchInput] = useState(() => getUrlParam('filter')); const deferredSearch = useDeferredValue(searchInput); const debounceRef = useRef>(); const setSearch = useCallback( (v: string) => { setSearchInput(v); clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => setSearchUrl(v), 300); }, [setSearchUrl], ); useEffect(() => { return () => clearTimeout(debounceRef.current); }, []); // Category dropdown — only shown when no external category filter is provided via props const hasExternalCategoryFilter = !!(categoryProp || categoriesProp); const [selectedCategoryRaw, setSelectedCategoryRaw] = useUrlState('cat'); const selectedCategory = hasExternalCategoryFilter ? 'all' : selectedCategoryRaw || 'all'; const setSelectedCategory = useCallback( (v: string) => setSelectedCategoryRaw(v === 'all' ? '' : v), [setSelectedCategoryRaw], ); const [selectedSyntaxRaw, setSelectedSyntax] = useUrlState('syntax'); const selectedSyntax = selectedSyntaxRaw || 'javascript'; const activeCategoryOption = selectedCategory !== 'all' ? CATEGORY_OPTIONS.find(c => c.value === selectedCategory) : null; const activeCategory = activeCategoryOption?.category ?? categoryProp; const activeCategories = activeCategoryOption?.categories ?? categoriesProp; const activeAllowedTokens = activeCategoryOption?.allowedTokens ?? allowedTokensProp; const allTokens = useMemo( () => collectTokens({ allTokens: tokens as Record, resolveValue: (token: Token) => { try { return `${getToken(theme, colorScheme, token)}`; } catch { return '?'; } }, category: activeCategory, categories: activeCategories, nameFilter, allowedTokens: activeAllowedTokens, }), [theme, colorScheme, activeCategory, activeCategories, nameFilter, activeAllowedTokens], ); const filtered = useMemo( () => filterTokens(allTokens, { search: deferredSearch, showInternal: true, allowedGroups, groupBySegment, }), [allTokens, deferredSearch, allowedGroups, groupBySegment], ); const visibleTokens = useMemo( () => selectedSyntax === 'figma' ? filtered.filter(t => !!getFigmaTokenName(t.name)) : filtered, [filtered, selectedSyntax], ); const isPending = searchInput !== deferredSearch; return ( Token Explorer {!hasExternalCategoryFilter && ( All {CATEGORY_OPTIONS.map(cat => ( {cat.label} ))} )} JavaScript SCSS LESS Figma {visibleTokens.length} {visibleTokens.length === 1 ? 'token' : 'tokens'} } assistiveText="Share the current filter" onClick={async () => { try { const url = new URL( window.top?.location.href || window.location.href, ); await navigator.clipboard.writeText(url.toString()); showToast('URL copied to clipboard'); } catch { showToast('Failed to copy URL to clipboard', { variant: 'critical', }); } }} /> {visibleTokens.map((t, i) => { const { less, scss } = getTokenAlternativeSyntaxes(t.name); const figmaName = getFigmaTokenName(t.name); const displayName = selectedSyntax === 'scss' ? scss : selectedSyntax === 'less' ? less : selectedSyntax === 'figma' ? figmaName! : t.name; const headerOptions = groupBySegment !== undefined ? { groupBySegment, allowedGroups: allowedGroups } : undefined; let sectionHeader: React.ReactNode = null; if (headerOptions) { const prevName = i > 0 ? visibleTokens[i - 1].name : undefined; if (shouldShowSectionHeader(t.name, prevName, headerOptions)) { const group = getGroupName(t.name, headerOptions); sectionHeader = ( ); } } return ( {sectionHeader} ); })}
Token Value Preview
{group}
{displayName} {t.value}
); };