From 4e8213ffb20f0315b43bccbf0888f12a069aeab4 Mon Sep 17 00:00:00 2001 From: Herberts Markuns Date: Mon, 17 Aug 2026 10:33:55 +0300 Subject: [PATCH 1/2] #426 #427 #428 Grouping, search improvement, unicode for vaadin icons --- frontend/demo/foundation/icons-preview.ts | 159 ++++++++++++++++++++-- 1 file changed, 145 insertions(+), 14 deletions(-) diff --git a/frontend/demo/foundation/icons-preview.ts b/frontend/demo/foundation/icons-preview.ts index bdc98af76c..b525b7fb19 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,11 +18,33 @@ const IconSets = { export type IconSetType = 'lumo' | 'vaadin'; +interface VaadinIconMeta { + name: string; + code: string; + categories: string[]; + meta: string[]; +} + +const OTHER_CATEGORY = 'Other'; + +const vaadinIconMetaByName = new Map( + (vaadinFontIcons as VaadinIconMeta[]).map((icon) => [icon.name, icon]) +); + +interface IconEntry { + fullName: string; + searchText: string; + code?: string; +} + @customElement('icons-preview') export class IconsPreview extends LitElement { @state() iconNames: string[] | undefined; + @state() + categorizedIcons: Map | undefined; + @property({ type: String, attribute: 'iconset-type' }) iconsetType: IconSetType = 'vaadin'; @@ -33,15 +56,44 @@ export class IconsPreview extends LitElement { } protected firstUpdated() { - this.iconNames = Object.keys(IconSets[this.iconsetType]._icons).map( - (name) => `${this.iconsetType}:${name}` - ); + const bareNames = Object.keys(IconSets[this.iconsetType]._icons); + this.iconNames = bareNames.map((name) => `${this.iconsetType}:${name}`); + + if (this.iconsetType === 'vaadin') { + const categories = new Map(); + bareNames.forEach((name) => { + const meta = vaadinIconMetaByName.get(name); + const entry: IconEntry = { + fullName: `${this.iconsetType}:${name}`, + searchText: [name, ...(meta?.meta ?? [])].join(' ').toLowerCase(), + code: meta?.code, + }; + const iconCategories = meta?.categories?.length ? meta.categories : [OTHER_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', () => { + const term = this.search.value.toLowerCase(); this.querySelectorAll('.docs-icon-preview').forEach((icon) => { - icon.classList.toggle( - 'hidden', - !icon.className.toLowerCase().includes(this.search.value.toLowerCase()) - ); + const searchText = (icon as HTMLElement).dataset.search ?? ''; + icon.classList.toggle('hidden', !searchText.includes(term)); + }); + this.querySelectorAll('.docs-icon-category').forEach((section) => { + const hasVisibleIcon = section.querySelector('.docs-icon-preview:not(.hidden)'); + section.classList.toggle('hidden', !hasVisibleIcon); }); }); } @@ -62,10 +114,7 @@ export class IconsPreview extends LitElement { border-radius: var(--docs-border-radius-l); } - .icons-preview ul { - display: grid; - list-style: none; - grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); + .docs-icon-scroll { width: 100%; max-height: 60vh; margin: 0; @@ -73,10 +122,44 @@ export class IconsPreview extends LitElement { overflow: auto; } + .docs-icon-category .docs-icon-grid { + display: grid; + list-style: none; + grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr)); + width: 100%; + margin: 0; + padding: 0; + padding-top: 20px; + padding-inline-end: 0; + border-top: 1px solid var(--docs-divider-color-1); + } + .icons-preview li { display: block; } + .docs-icon-category { + border: 1px solid var(--docs-divider-color-1); + border-radius: var(--docs-border-radius-l); + margin: 10px; + } + + .docs-icon-category.hidden { + display: none; + } + + .docs-icon-category .docs-icon-category-heading { + padding: var(--docs-space-m) var(--docs-space-m); + margin: 0; + font-size: var(--docs-font-size-m); + font-weight: var(--docs-font-weight-strong); + color: var(--docs-body-text-color); + } + + .docs-icon-category:first-of-type .docs-icon-category-heading { + margin-top: 0; + } + .docs-icon-preview { text-align: center; padding-bottom: var(--docs-space-l); @@ -84,8 +167,9 @@ export class IconsPreview extends LitElement { } .docs-icon-preview vaadin-icon { - outline: 1px dashed var(--docs-divider-color-2); margin-bottom: 0.5em; + height: 24px; + width: 24px; } .docs-icon-preview.hidden { @@ -98,6 +182,12 @@ export class IconsPreview extends LitElement { color: var(--docs-secondary-text-color); } + .docs-icon-preview-code { + display: block; + font-size: var(--docs-font-size-2xs); + color: var(--docs-secondary-text-color); + } + .docs-icon-search { flex: none; max-width: 20em; @@ -118,10 +208,16 @@ export class IconsPreview extends LitElement { aria-label="Search all icons" placeholder="Search all icons" /> -
    + ${this.iconsetType === 'vaadin' ? this.renderCategorized() : this.renderFlat()} + `; + } + + private renderFlat() { + return html` +
      ${this.iconNames?.map( (name: string) => html` -
    • +
    • ${name}
    • @@ -130,4 +226,39 @@ export class IconsPreview extends LitElement {
    `; } + + 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}` + : '' + } +
    • + ` + )} +
    +
    + ` + ) + } +
    + `; + } } From a271876c96ef09dfe0e79109faacd520a39f43d9 Mon Sep 17 00:00:00 2001 From: Herberts Markuns Date: Tue, 18 Aug 2026 11:36:29 +0300 Subject: [PATCH 2/2] #426 #427 #428 No grouping on search, add icon size selector, search result counter --- frontend/demo/foundation/icons-preview.ts | 186 ++++++++++++++++------ 1 file changed, 137 insertions(+), 49 deletions(-) diff --git a/frontend/demo/foundation/icons-preview.ts b/frontend/demo/foundation/icons-preview.ts index b525b7fb19..94279f371d 100644 --- a/frontend/demo/foundation/icons-preview.ts +++ b/frontend/demo/foundation/icons-preview.ts @@ -27,6 +27,11 @@ interface VaadinIconMeta { 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]) ); @@ -37,38 +42,55 @@ interface IconEntry { 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() { const bareNames = Object.keys(IconSets[this.iconsetType]._icons); - this.iconNames = bareNames.map((name) => `${this.iconsetType}:${name}`); + + 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(); - bareNames.forEach((name) => { - const meta = vaadinIconMetaByName.get(name); - const entry: IconEntry = { - fullName: `${this.iconsetType}:${name}`, - searchText: [name, ...(meta?.meta ?? [])].join(' ').toLowerCase(), - code: meta?.code, - }; - const iconCategories = meta?.categories?.length ? meta.categories : [OTHER_CATEGORY]; + 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, []); @@ -86,24 +108,28 @@ export class IconsPreview extends LitElement { } this.search.addEventListener('input', () => { - const term = this.search.value.toLowerCase(); - this.querySelectorAll('.docs-icon-preview').forEach((icon) => { - const searchText = (icon as HTMLElement).dataset.search ?? ''; - icon.classList.toggle('hidden', !searchText.includes(term)); - }); - this.querySelectorAll('.docs-icon-category').forEach((section) => { - const hasVisibleIcon = section.querySelector('.docs-icon-preview:not(.hidden)'); - section.classList.toggle('hidden', !hasVisibleIcon); - }); + 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.iconsetType === 'vaadin' ? this.renderCategorized() : this.renderFlat()} +
    + +
    + ${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 renderFlat() { + private renderGrid(entries: IconEntry[]) { return html`
      - ${this.iconNames?.map( - (name: string) => html` -
    • - - ${name} + ${entries.map( + (icon) => html` +
    • + + ${icon.fullName} + ${icon.code ? html`\\${icon.code}` : ''}
    • ` )} @@ -239,10 +330,7 @@ export class IconsPreview extends LitElement {
        ${icons.map( (icon) => html` -
      • +
      • ${icon.fullName} ${