You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
7.2 KiB
Vue
237 lines
7.2 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, watch } from 'vue'
|
|
|
|
import { CIcon, CSeparator } from '@/index'
|
|
import CCodeBlock from '@/documentation/CCodeBlock.vue'
|
|
|
|
import { emojiCatalog, emojiCatalogVersion } from './emoji-catalog'
|
|
import type { EmojiCatalogEntry } from './emoji-catalog'
|
|
import { emojiSearchAliases } from './emoji-search-aliases'
|
|
|
|
const pageSize = 120
|
|
const search = ref('')
|
|
const selectedGroup = ref('All')
|
|
const selectedSubgroup = ref('All')
|
|
const currentPage = ref(1)
|
|
const copyStatus = ref('')
|
|
|
|
const emojiUsage = `<CIcon>🚚</CIcon>
|
|
<CIcon display="circle" size="20px">📦</CIcon>
|
|
<CIcon :rotate="-0.25" label="Launching">🚀</CIcon>`
|
|
|
|
const unicodeGroups = Array.from(new Set(emojiCatalog.map((entry) => entry.group)))
|
|
const groups = ['All', ...unicodeGroups]
|
|
|
|
const groupCounts = new Map<string, number>()
|
|
groupCounts.set('All', emojiCatalog.length)
|
|
|
|
for (const entry of emojiCatalog) {
|
|
groupCounts.set(entry.group, (groupCounts.get(entry.group) ?? 0) + 1)
|
|
}
|
|
|
|
const subgroups = computed(() => {
|
|
if (selectedGroup.value === 'All') return ['All']
|
|
|
|
return [
|
|
'All',
|
|
...new Set(
|
|
emojiCatalog
|
|
.filter((entry) => entry.group === selectedGroup.value)
|
|
.map((entry) => entry.subgroup),
|
|
),
|
|
]
|
|
})
|
|
|
|
const filteredEntries = computed(() => {
|
|
const query = search.value.trim().toLocaleLowerCase()
|
|
|
|
return emojiCatalog.filter((entry) => {
|
|
if (selectedGroup.value !== 'All' && entry.group !== selectedGroup.value) return false
|
|
if (selectedSubgroup.value !== 'All' && entry.subgroup !== selectedSubgroup.value) return false
|
|
if (!query) return true
|
|
|
|
const aliases = emojiSearchAliases[entry.name] ?? []
|
|
|
|
return (
|
|
entry.name.toLocaleLowerCase().includes(query) ||
|
|
entry.group.toLocaleLowerCase().includes(query) ||
|
|
entry.subgroup.toLocaleLowerCase().includes(query) ||
|
|
aliases.some((alias) => alias.includes(query))
|
|
)
|
|
})
|
|
})
|
|
|
|
const pageCount = computed(() => Math.max(1, Math.ceil(filteredEntries.value.length / pageSize)))
|
|
|
|
const pageEntries = computed(() => {
|
|
const start = (currentPage.value - 1) * pageSize
|
|
return filteredEntries.value.slice(start, start + pageSize)
|
|
})
|
|
|
|
const groupedPageEntries = computed(() => {
|
|
const sections: Array<{
|
|
key: string
|
|
group: string
|
|
subgroup: string
|
|
entries: EmojiCatalogEntry[]
|
|
}> = []
|
|
|
|
for (const entry of pageEntries.value) {
|
|
const key = `${entry.group}/${entry.subgroup}`
|
|
let section = sections.at(-1)
|
|
|
|
if (section?.key !== key) {
|
|
section = { key, group: entry.group, subgroup: entry.subgroup, entries: [] }
|
|
sections.push(section)
|
|
}
|
|
|
|
section.entries.push(entry)
|
|
}
|
|
|
|
return sections
|
|
})
|
|
|
|
function selectGroup(group: string) {
|
|
selectedGroup.value = group
|
|
selectedSubgroup.value = 'All'
|
|
}
|
|
|
|
async function copyEmoji(entry: EmojiCatalogEntry) {
|
|
try {
|
|
await navigator.clipboard.writeText(entry.emoji)
|
|
copyStatus.value = `Copied ${entry.emoji} — ${entry.name}`
|
|
} catch {
|
|
copyStatus.value = 'Copy failed. Select the emoji text and copy it manually.'
|
|
}
|
|
}
|
|
|
|
watch([selectedGroup, selectedSubgroup, search], () => {
|
|
currentPage.value = 1
|
|
})
|
|
|
|
watch(search, (value) => {
|
|
if (!value.trim()) return
|
|
selectedGroup.value = 'All'
|
|
selectedSubgroup.value = 'All'
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<article class="glyph-page">
|
|
<header class="page-header">
|
|
<div>
|
|
<p class="category">Visual</p>
|
|
<h1>Emoji</h1>
|
|
</div>
|
|
<p>
|
|
Unicode Emoji {{ emojiCatalogVersion }} contains {{ emojiCatalog.length.toLocaleString() }}
|
|
emoji characters and sequences. Render them with <code>CIcon</code>; artwork and color
|
|
depend on the operating system.
|
|
</p>
|
|
</header>
|
|
|
|
<CSeparator />
|
|
|
|
<section class="section">
|
|
<h2>Unicode catalog</h2>
|
|
<p>
|
|
Browse the official Unicode groups and subgroups, then copy an emoji directly into a
|
|
<code>CIcon</code> default slot.
|
|
</p>
|
|
|
|
<div class="category-grid" aria-label="Emoji categories">
|
|
<button
|
|
v-for="group in groups"
|
|
:key="group"
|
|
type="button"
|
|
class="category-button"
|
|
:class="{ 'is-active': selectedGroup === group }"
|
|
:aria-pressed="selectedGroup === group"
|
|
@click="selectGroup(group)"
|
|
>
|
|
<span>{{ group }}</span>
|
|
<span class="count">{{ groupCounts.get(group) }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="filters">
|
|
<label>
|
|
<span>Search</span>
|
|
<input v-model="search" type="search" placeholder="Name, group, or subgroup" />
|
|
</label>
|
|
|
|
<label>
|
|
<span>Subgroup</span>
|
|
<select v-model="selectedSubgroup" :disabled="selectedGroup === 'All'">
|
|
<option v-for="subgroup in subgroups" :key="subgroup" :value="subgroup">
|
|
{{ subgroup }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
|
|
<div class="result-count">
|
|
{{ filteredEntries.length.toLocaleString() }} result<span v-if="filteredEntries.length !== 1">s</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="groupedPageEntries.length" class="catalog">
|
|
<section v-for="section in groupedPageEntries" :key="section.key" class="subgroup">
|
|
<header class="subgroup-header">
|
|
<strong>{{ section.subgroup }}</strong>
|
|
<span>{{ section.group }}</span>
|
|
</header>
|
|
|
|
<div class="grid is-wide">
|
|
<button
|
|
v-for="entry in section.entries"
|
|
:key="`${entry.emoji}-${entry.name}`"
|
|
type="button"
|
|
class="card interactive"
|
|
:title="`Copy ${entry.name} · Emoji ${entry.version}`"
|
|
@click="copyEmoji(entry)"
|
|
>
|
|
<CIcon size="22px">{{ entry.emoji }}</CIcon>
|
|
<span class="details">
|
|
<span>{{ entry.name }}</span>
|
|
<code>E{{ entry.version }}</code>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<p v-else class="empty">No emoji match the current filters.</p>
|
|
|
|
<p class="copy-status" aria-live="polite">{{ copyStatus }}</p>
|
|
|
|
<nav v-if="pageCount > 1" class="pagination" aria-label="Emoji catalog pages">
|
|
<button type="button" :disabled="currentPage === 1" @click="currentPage--">Previous</button>
|
|
<span>Page {{ currentPage }} of {{ pageCount }}</span>
|
|
<button type="button" :disabled="currentPage === pageCount" @click="currentPage++">Next</button>
|
|
</nav>
|
|
</section>
|
|
|
|
<section class="section">
|
|
<h2>Presentation</h2>
|
|
<p>Paste the selected character directly into the component's default slot.</p>
|
|
<CCodeBlock :code="emojiUsage" />
|
|
<div class="example-row">
|
|
<div class="example">
|
|
<CIcon size="16px">🍇</CIcon>
|
|
<code><CIcon>🍇</CIcon></code>
|
|
</div>
|
|
<div class="example">
|
|
<CIcon display="circle" size="18px">📦</CIcon>
|
|
<code>18px circle</code>
|
|
</div>
|
|
<div class="example">
|
|
<CIcon size="24px" :rotate="-0.25" label="Launching">🚀</CIcon>
|
|
<code>:rotate="-0.25"</code>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</article>
|
|
</template>
|
|
|
|
<style scoped lang="scss" src="./glyph-demo.scss"></style>
|