diff --git a/frontend/demo/foundation/icons-preview.ts b/frontend/demo/foundation/icons-preview.ts index bdc98af76c..94279f371d 100644 --- a/frontend/demo/foundation/icons-preview.ts +++ b/frontend/demo/foundation/icons-preview.ts @@ -4,6 +4,7 @@ import '@vaadin/vaadin-lumo-styles/vaadin-iconset'; import { html, LitElement } from 'lit'; import { customElement, property, query, state } from 'lit/decorators.js'; import { Iconset } from '@vaadin/icon/vaadin-iconset.js'; +import vaadinFontIcons from '@vaadin/icons/assets/vaadin-font-icons.json'; type VaadinIconset = Iconset & { _icons: string[] }; @@ -17,41 +18,118 @@ const IconSets = { export type IconSetType = 'lumo' | 'vaadin'; +interface VaadinIconMeta { + name: string; + code: string; + categories: string[]; + meta: string[]; +} + +const OTHER_CATEGORY = 'Other'; + +// "Items" is a one-icon typo for "Item" in the upstream icon metadata. +const CATEGORY_ALIASES: Record = { + Items: 'Item', +}; + +const vaadinIconMetaByName = new Map( + (vaadinFontIcons as VaadinIconMeta[]).map((icon) => [icon.name, icon]) +); + +interface IconEntry { + fullName: string; + searchText: string; + code?: string; +} + +const ICON_SIZES = [16, 20, 24, 32]; +const DEFAULT_ICON_SIZE = 24; + @customElement('icons-preview') export class IconsPreview extends LitElement { @state() - iconNames: string[] | undefined; + iconEntries: IconEntry[] | undefined; + + @state() + categorizedIcons: Map | undefined; + + @state() + searchTerm = ''; + + @state() + iconSize = DEFAULT_ICON_SIZE; @property({ type: String, attribute: 'iconset-type' }) iconsetType: IconSetType = 'vaadin'; - @query('input') + @query('input.docs-icon-search') private search!: HTMLInputElement; + @query('.docs-icon-size-picker') + private sizePicker!: HTMLFieldSetElement; + protected override createRenderRoot() { return this; } protected firstUpdated() { - this.iconNames = Object.keys(IconSets[this.iconsetType]._icons).map( - (name) => `${this.iconsetType}:${name}` - ); - this.search.addEventListener('input', () => { - this.querySelectorAll('.docs-icon-preview').forEach((icon) => { - icon.classList.toggle( - 'hidden', - !icon.className.toLowerCase().includes(this.search.value.toLowerCase()) + const bareNames = Object.keys(IconSets[this.iconsetType]._icons); + + this.iconEntries = bareNames.map((name) => { + const meta = this.iconsetType === 'vaadin' ? vaadinIconMetaByName.get(name) : undefined; + return { + fullName: `${this.iconsetType}:${name}`, + searchText: [name, ...(meta?.meta ?? [])].join(' ').toLowerCase(), + code: meta?.code, + }; + }); + + if (this.iconsetType === 'vaadin') { + const categories = new Map(); + this.iconEntries.forEach((entry, i) => { + const meta = vaadinIconMetaByName.get(bareNames[i]); + const iconCategories = (meta?.categories?.length ? meta.categories : [OTHER_CATEGORY]).map( + (category) => CATEGORY_ALIASES[category] ?? category ); + iconCategories.forEach((category) => { + if (!categories.has(category)) { + categories.set(category, []); + } + categories.get(category)!.push(entry); + }); }); + this.categorizedIcons = new Map( + [...categories.entries()].sort(([a], [b]) => { + if (a === OTHER_CATEGORY) return 1; + if (b === OTHER_CATEGORY) return -1; + return a.localeCompare(b); + }) + ); + } + + this.search.addEventListener('input', () => { + this.searchTerm = this.search.value; + }); + + this.sizePicker.addEventListener('change', (event) => { + this.iconSize = Number((event.target as HTMLInputElement).value); + this.style.setProperty('--vaadin-icon-size', `${this.iconSize}px`); }); } override connectedCallback() { super.connectedCallback(); this.classList.add('icons-preview'); + this.style.setProperty('--vaadin-icon-size', `${this.iconSize}px`); } protected override render() { + const term = this.searchTerm.trim().toLowerCase(); + const isSearching = term.length > 0; + const matches = isSearching + ? (this.iconEntries ?? []).filter((entry) => entry.searchText.includes(term)) + : []; + return html` - -
    - ${this.iconNames?.map( - (name: string) => html` -
  • - - ${name} +
    + +
    + ${ICON_SIZES.map( + (size) => html` + + ` + )} +
    +
    + + ${ + isSearching + ? html` +

    + ${matches.length} icon${matches.length === 1 ? '' : 's'} found +

    + ${this.renderGrid(matches)} + ` + : this.iconsetType === 'vaadin' + ? this.renderCategorized() + : this.renderGrid(this.iconEntries ?? []) + } + `; + } + + private renderGrid(entries: IconEntry[]) { + return html` +
      + ${entries.map( + (icon) => html` +
    • + + ${icon.fullName} + ${icon.code ? html`\\${icon.code}` : ''}
    • ` )}
    `; } + + private renderCategorized() { + return html` +
    + ${ + this.categorizedIcons && + [...this.categorizedIcons.entries()].map( + ([category, icons]) => html` +
    +

    ${category}

    +
      + ${icons.map( + (icon) => html` +
    • + + ${icon.fullName} + ${ + icon.code + ? html`\\${icon.code}` + : '' + } +
    • + ` + )} +
    +
    + ` + ) + } +
    + `; + } }