mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-02 04:43:58 +00:00
OpenEmoji catalog: browse and install by network
Reads platforms.json from the set: a network picker grouped by kind (custom emoji, sticker packs, images to post), and for the chosen network its verified limits, install steps, packs with sizes, Show (in the grid) and Download (release assets), the grid narrowed to what its bundles hold with each emoji's name on that network, and the detail panel saying which pack holds it. A network that names what it is for opens on it: X opens on country flags. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f80488ce92
commit
cbe6b1cfd5
2 changed files with 414 additions and 6 deletions
|
|
@ -447,3 +447,161 @@
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.networks {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-bottom: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.networkRow {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.networkRow > span:first-child {
|
||||||
|
min-width: 7.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kind_emoji,
|
||||||
|
.kind_stickers,
|
||||||
|
.kind_media {
|
||||||
|
font-size: 0.66rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.05rem 0.4rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kind_emoji {
|
||||||
|
background: #e3f4ec;
|
||||||
|
color: #0a6a4c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kind_stickers {
|
||||||
|
background: #efe8fb;
|
||||||
|
color: #5b3aa6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kind_media {
|
||||||
|
background: #fdf0dc;
|
||||||
|
color: #8a5a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chipOn .kind_emoji,
|
||||||
|
.chipOn .kind_stickers,
|
||||||
|
.chipOn .kind_media {
|
||||||
|
background: rgb(255 255 255 / 22%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
background: #fff;
|
||||||
|
padding: 1rem 1.1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelHead h3 {
|
||||||
|
margin: 0 0 0.35rem;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelHead p {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
color: #41505d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkish {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-right: 0.6rem;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelBody {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
gap: 1rem 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.2rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: #26323c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps li + li {
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packs table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packs th {
|
||||||
|
text-align: left;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0 0.5rem 0.35rem 0;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.packs td {
|
||||||
|
padding: 0.35rem 0.5rem 0.35rem 0;
|
||||||
|
border-bottom: 1px solid #f0f2ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packOn td {
|
||||||
|
background: #eef8f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packActions {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onNetwork {
|
||||||
|
font-size: 0.86rem;
|
||||||
|
margin: 0 0 0.8rem;
|
||||||
|
color: #26323c;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.panelBody {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.networkRow {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
.networkRow > * {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,41 @@ type Emoji = {
|
||||||
subgroup: string;
|
subgroup: string;
|
||||||
unicode: string;
|
unicode: string;
|
||||||
keywords?: string[];
|
keywords?: string[];
|
||||||
|
shortcodes?: string[];
|
||||||
base?: string;
|
base?: string;
|
||||||
png?: string;
|
png?: string;
|
||||||
svg?: string;
|
svg?: string;
|
||||||
webp?: string;
|
webp?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type NetworkPack = { id: string; title: string; subset: string; keys: string[]; file: string; bytes: number };
|
||||||
|
|
||||||
|
type Network = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
kind: "emoji" | "stickers" | "media";
|
||||||
|
size: number;
|
||||||
|
format: string;
|
||||||
|
max_bytes: number;
|
||||||
|
name_rule?: { max: number; pattern: string; prefix: string };
|
||||||
|
packSize?: number;
|
||||||
|
packsFrom?: string;
|
||||||
|
highlight?: { subgroups: string[]; why: string };
|
||||||
|
limits: string;
|
||||||
|
steps: string[];
|
||||||
|
docs: string[];
|
||||||
|
packs: NetworkPack[];
|
||||||
|
renamed: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Networks = { generated: string; download_base: string; platforms: Network[] };
|
||||||
|
|
||||||
|
const KIND_LABEL: Record<Network["kind"], string> = {
|
||||||
|
emoji: "Custom emoji",
|
||||||
|
stickers: "Sticker packs",
|
||||||
|
media: "Images to post"
|
||||||
|
};
|
||||||
|
|
||||||
type Manifest = {
|
type Manifest = {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
|
|
@ -63,9 +92,23 @@ type Filters = {
|
||||||
status: "" | "drawn" | "todo";
|
status: "" | "drawn" | "todo";
|
||||||
sort: "unicode" | "name" | "newest";
|
sort: "unicode" | "name" | "newest";
|
||||||
size: number;
|
size: number;
|
||||||
|
/** A network id from platforms.json, and optionally one of its packs. */
|
||||||
|
platform: string;
|
||||||
|
pack: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULTS: Filters = { q: "", group: "", sub: "", tone: "", version: "", status: "", sort: "unicode", size: 56 };
|
const DEFAULTS: Filters = {
|
||||||
|
q: "",
|
||||||
|
group: "",
|
||||||
|
sub: "",
|
||||||
|
tone: "",
|
||||||
|
version: "",
|
||||||
|
status: "",
|
||||||
|
sort: "unicode",
|
||||||
|
size: 56,
|
||||||
|
platform: "",
|
||||||
|
pack: ""
|
||||||
|
};
|
||||||
const PAGE = 360;
|
const PAGE = 360;
|
||||||
|
|
||||||
function readFilters(): Filters {
|
function readFilters(): Filters {
|
||||||
|
|
@ -81,7 +124,9 @@ function readFilters(): Filters {
|
||||||
version: params.get("v") ?? "",
|
version: params.get("v") ?? "",
|
||||||
status: status === "drawn" || status === "todo" ? status : "",
|
status: status === "drawn" || status === "todo" ? status : "",
|
||||||
sort: sort === "name" || sort === "newest" ? sort : "unicode",
|
sort: sort === "name" || sort === "newest" ? sort : "unicode",
|
||||||
size: size >= 32 && size <= 128 ? size : DEFAULTS.size
|
size: size >= 32 && size <= 128 ? size : DEFAULTS.size,
|
||||||
|
platform: params.get("platform") ?? "",
|
||||||
|
pack: params.get("pack") ?? ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,6 +140,8 @@ function writeFilters(f: Filters, selected: string | null): void {
|
||||||
if (f.status) params.set("status", f.status);
|
if (f.status) params.set("status", f.status);
|
||||||
if (f.sort !== "unicode") params.set("sort", f.sort);
|
if (f.sort !== "unicode") params.set("sort", f.sort);
|
||||||
if (f.size !== DEFAULTS.size) params.set("size", String(f.size));
|
if (f.size !== DEFAULTS.size) params.set("size", String(f.size));
|
||||||
|
if (f.platform) params.set("platform", f.platform);
|
||||||
|
if (f.pack) params.set("pack", f.pack);
|
||||||
if (selected) params.set("e", selected);
|
if (selected) params.set("e", selected);
|
||||||
const query = params.toString();
|
const query = params.toString();
|
||||||
window.history.replaceState(null, "", query ? `?${query}` : window.location.pathname);
|
window.history.replaceState(null, "", query ? `?${query}` : window.location.pathname);
|
||||||
|
|
@ -120,6 +167,7 @@ const fill = (template: string, size: number): string => template.replace("{size
|
||||||
|
|
||||||
export function Catalog(): ReactNode {
|
export function Catalog(): ReactNode {
|
||||||
const [manifest, setManifest] = useState<Manifest | null>(null);
|
const [manifest, setManifest] = useState<Manifest | null>(null);
|
||||||
|
const [networks, setNetworks] = useState<Networks | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [filters, setFilters] = useState<Filters>(DEFAULTS);
|
const [filters, setFilters] = useState<Filters>(DEFAULTS);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
@ -141,6 +189,11 @@ export function Catalog(): ReactNode {
|
||||||
})
|
})
|
||||||
.then(setManifest)
|
.then(setManifest)
|
||||||
.catch((reason: Error) => setError(reason.message));
|
.catch((reason: Error) => setError(reason.message));
|
||||||
|
// The per-network bundles are optional: without them the catalog still works.
|
||||||
|
fetch(`${SET_URL}/platforms.json`)
|
||||||
|
.then((response) => (response.ok ? (response.json() as Promise<Networks>) : null))
|
||||||
|
.then(setNetworks)
|
||||||
|
.catch(() => setNetworks(null));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// The search box types freely; the filter follows a beat behind.
|
// The search box types freely; the filter follows a beat behind.
|
||||||
|
|
@ -171,6 +224,7 @@ export function Catalog(): ReactNode {
|
||||||
setFilters((f) => {
|
setFilters((f) => {
|
||||||
const next = { ...f, [key]: value };
|
const next = { ...f, [key]: value };
|
||||||
if (key === "group") next.sub = "";
|
if (key === "group") next.sub = "";
|
||||||
|
if (key === "platform") next.pack = "";
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -217,6 +271,27 @@ export function Catalog(): ReactNode {
|
||||||
[index]
|
[index]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const network = useMemo(
|
||||||
|
() => networks?.platforms.find((p) => p.id === filters.platform) ?? null,
|
||||||
|
[networks, filters.platform]
|
||||||
|
);
|
||||||
|
|
||||||
|
/** What the chosen network's bundles hold (or one pack of them); null when no network is chosen. */
|
||||||
|
const networkKeys = useMemo(() => {
|
||||||
|
if (!network) return null;
|
||||||
|
const packs = filters.pack ? network.packs.filter((p) => p.id === filters.pack) : network.packs;
|
||||||
|
return new Set(packs.flatMap((p) => p.keys));
|
||||||
|
}, [network, filters.pack]);
|
||||||
|
|
||||||
|
/** The name an emoji goes by on the chosen network. */
|
||||||
|
const nameOn = useCallback(
|
||||||
|
(e: Emoji): string | null => {
|
||||||
|
if (!network?.name_rule) return null;
|
||||||
|
return network.renamed[e.key] ?? e.shortcodes?.[0] ?? null;
|
||||||
|
},
|
||||||
|
[network]
|
||||||
|
);
|
||||||
|
|
||||||
const results = useMemo(() => {
|
const results = useMemo(() => {
|
||||||
if (!manifest || !index) return [];
|
if (!manifest || !index) return [];
|
||||||
// Tone swaps come from variants the other filters may have excluded
|
// Tone swaps come from variants the other filters may have excluded
|
||||||
|
|
@ -226,13 +301,21 @@ export function Catalog(): ReactNode {
|
||||||
let list = toneView(base, filters.tone);
|
let list = toneView(base, filters.tone);
|
||||||
if (filters.status === "drawn") list = list.filter((e) => e.png);
|
if (filters.status === "drawn") list = list.filter((e) => e.png);
|
||||||
if (filters.status === "todo") list = list.filter((e) => !e.png);
|
if (filters.status === "todo") list = list.filter((e) => !e.png);
|
||||||
|
if (networkKeys) {
|
||||||
|
// A network's bundles decide; with a tone chosen, a variant outside
|
||||||
|
// them falls back to its base when the base is in.
|
||||||
|
list = list
|
||||||
|
.map((e) => (networkKeys.has(e.key) ? e : e.base && networkKeys.has(e.base) ? index.byKey.get(e.base)! : null))
|
||||||
|
.filter((e): e is Emoji => Boolean(e));
|
||||||
|
list = [...new Map(list.map((e) => [e.key, e])).values()];
|
||||||
|
}
|
||||||
if (filters.sort === "name") list = [...list].sort((a, b) => a.name.localeCompare(b.name));
|
if (filters.sort === "name") list = [...list].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
if (filters.sort === "newest")
|
if (filters.sort === "newest")
|
||||||
list = [...list].sort(
|
list = [...list].sort(
|
||||||
(a, b) => compareVersions(b.unicode, a.unicode) || (index.order.get(a.key) ?? 0) - (index.order.get(b.key) ?? 0)
|
(a, b) => compareVersions(b.unicode, a.unicode) || (index.order.get(a.key) ?? 0) - (index.order.get(b.key) ?? 0)
|
||||||
);
|
);
|
||||||
return list;
|
return list;
|
||||||
}, [manifest, index, filters, matchesBase, toneView]);
|
}, [manifest, index, filters, matchesBase, toneView, networkKeys]);
|
||||||
|
|
||||||
const groupCounts = useMemo(() => {
|
const groupCounts = useMemo(() => {
|
||||||
const counts = new Map<string, number>();
|
const counts = new Map<string, number>();
|
||||||
|
|
@ -241,9 +324,12 @@ export function Catalog(): ReactNode {
|
||||||
manifest.emoji.filter((e) => matchesBase(e, filters, { group: true })),
|
manifest.emoji.filter((e) => matchesBase(e, filters, { group: true })),
|
||||||
filters.tone
|
filters.tone
|
||||||
);
|
);
|
||||||
for (const e of list) counts.set(e.group, (counts.get(e.group) ?? 0) + 1);
|
for (const e of list) {
|
||||||
|
if (networkKeys && !networkKeys.has(e.key)) continue;
|
||||||
|
counts.set(e.group, (counts.get(e.group) ?? 0) + 1);
|
||||||
|
}
|
||||||
return counts;
|
return counts;
|
||||||
}, [manifest, filters, matchesBase, toneView]);
|
}, [manifest, filters, matchesBase, toneView, networkKeys]);
|
||||||
|
|
||||||
const subgroups = useMemo(() => {
|
const subgroups = useMemo(() => {
|
||||||
if (!manifest || !filters.group) return [];
|
if (!manifest || !filters.group) return [];
|
||||||
|
|
@ -305,6 +391,60 @@ export function Catalog(): ReactNode {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{networks ? (
|
||||||
|
<div className={styles.networks} role="group" aria-label="Network">
|
||||||
|
<div className={styles.networkRow}>
|
||||||
|
<span className={styles.rowLabel}>Install on</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={!filters.platform ? styles.chipOn : styles.chip}
|
||||||
|
onClick={() => set("platform", "")}
|
||||||
|
>
|
||||||
|
Any network
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{(["emoji", "stickers", "media"] as const).map((kind) => (
|
||||||
|
<div key={kind} className={styles.networkRow}>
|
||||||
|
<span className={styles[`kind_${kind}`]}>{KIND_LABEL[kind]}</span>
|
||||||
|
{networks.platforms
|
||||||
|
.filter((p) => p.kind === kind)
|
||||||
|
.map((p) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={p.id}
|
||||||
|
className={filters.platform === p.id ? styles.chipOn : styles.chip}
|
||||||
|
onClick={() => {
|
||||||
|
if (filters.platform === p.id) {
|
||||||
|
set("platform", "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A network that names what it is for (X: flags) opens on it.
|
||||||
|
const sub = p.highlight?.subgroups[0] ?? "";
|
||||||
|
const group = sub ? (manifest.emoji.find((e) => e.subgroup === sub)?.group ?? "") : "";
|
||||||
|
setFilters((f) => ({ ...f, platform: p.id, pack: "", group: sub ? group : f.group, sub: sub || f.sub }));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{p.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{network && networks ? (
|
||||||
|
<NetworkPanel
|
||||||
|
network={network}
|
||||||
|
repoRaw={networks.download_base}
|
||||||
|
activePack={filters.pack}
|
||||||
|
onPack={(id) => set("pack", filters.pack === id ? "" : id)}
|
||||||
|
onHighlight={(sub) => {
|
||||||
|
const e = manifest.emoji.find((x) => x.subgroup === sub);
|
||||||
|
setFilters((f) => ({ ...f, group: e?.group ?? "", sub }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className={styles.toolbar}>
|
<div className={styles.toolbar}>
|
||||||
<input
|
<input
|
||||||
className={styles.search}
|
className={styles.search}
|
||||||
|
|
@ -455,7 +595,9 @@ export function Catalog(): ReactNode {
|
||||||
{e.char}
|
{e.char}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{filters.size >= 48 ? <span className={styles.label}>{e.name}</span> : null}
|
{filters.size >= 48 ? (
|
||||||
|
<span className={styles.label}>{nameOn(e) ? `:${nameOn(e)}:` : e.name}</span>
|
||||||
|
) : null}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
@ -466,6 +608,8 @@ export function Catalog(): ReactNode {
|
||||||
{current ? (
|
{current ? (
|
||||||
<Detail
|
<Detail
|
||||||
emoji={current}
|
emoji={current}
|
||||||
|
network={network}
|
||||||
|
nameOnNetwork={nameOn(current)}
|
||||||
variants={index.variants.get(current.base ?? current.key) ?? []}
|
variants={index.variants.get(current.base ?? current.key) ?? []}
|
||||||
base={current.base ? (index.byKey.get(current.base) ?? null) : null}
|
base={current.base ? (index.byKey.get(current.base) ?? null) : null}
|
||||||
onSelect={setSelected}
|
onSelect={setSelected}
|
||||||
|
|
@ -480,8 +624,97 @@ export function Catalog(): ReactNode {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NetworkPanel({
|
||||||
|
network,
|
||||||
|
repoRaw,
|
||||||
|
activePack,
|
||||||
|
onPack,
|
||||||
|
onHighlight
|
||||||
|
}: {
|
||||||
|
network: Network;
|
||||||
|
repoRaw: string;
|
||||||
|
activePack: string;
|
||||||
|
onPack: (id: string) => void;
|
||||||
|
onHighlight: (subgroup: string) => void;
|
||||||
|
}): ReactNode {
|
||||||
|
const mb = (bytes: number) => (bytes >= 1048576 ? `${(bytes / 1048576).toFixed(1)} MB` : `${Math.max(1, Math.round(bytes / 1024))} KB`);
|
||||||
|
return (
|
||||||
|
<section className={styles.panel} aria-label={`Install on ${network.name}`}>
|
||||||
|
<div className={styles.panelHead}>
|
||||||
|
<h3>
|
||||||
|
{network.name} <span className={styles[`kind_${network.kind}`]}>{KIND_LABEL[network.kind]}</span>
|
||||||
|
</h3>
|
||||||
|
<p>{network.limits}</p>
|
||||||
|
{network.highlight ? (
|
||||||
|
<p className={styles.highlight}>
|
||||||
|
{network.highlight.why}{" "}
|
||||||
|
{network.highlight.subgroups.map((sub) => (
|
||||||
|
<button type="button" key={sub} className={styles.linkish} onClick={() => onHighlight(sub)}>
|
||||||
|
{sub.replace(/-/g, " ")}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className={styles.panelBody}>
|
||||||
|
<ol className={styles.steps}>
|
||||||
|
{network.steps.map((step) => (
|
||||||
|
<li key={step}>{step}</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
{network.packs.length ? (
|
||||||
|
<div className={styles.packs}>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Pack</th>
|
||||||
|
<th>{network.kind === "stickers" ? "Stickers" : "Emoji"}</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{network.packs.map((pack) => (
|
||||||
|
<tr key={pack.id} className={activePack === pack.id ? styles.packOn : undefined}>
|
||||||
|
<td>{pack.title}</td>
|
||||||
|
<td>{pack.keys.length.toLocaleString()}</td>
|
||||||
|
<td>{mb(pack.bytes)}</td>
|
||||||
|
<td className={styles.packActions}>
|
||||||
|
<button type="button" className={styles.linkish} onClick={() => onPack(pack.id)}>
|
||||||
|
{activePack === pack.id ? "Show all" : "Show"}
|
||||||
|
</button>
|
||||||
|
<a href={`${repoRaw}/${pack.file}`} download>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{network.docs.length ? (
|
||||||
|
<p className={styles.docs}>
|
||||||
|
Official docs:{" "}
|
||||||
|
{network.docs.map((url, i) => (
|
||||||
|
<span key={url}>
|
||||||
|
{i ? ", " : ""}
|
||||||
|
<a href={url} rel="noopener noreferrer" target="_blank">
|
||||||
|
{new URL(url).hostname.replace(/^www\./, "")}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Detail({
|
function Detail({
|
||||||
emoji,
|
emoji,
|
||||||
|
network,
|
||||||
|
nameOnNetwork,
|
||||||
variants,
|
variants,
|
||||||
base,
|
base,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
|
@ -489,6 +722,8 @@ function Detail({
|
||||||
onClose
|
onClose
|
||||||
}: {
|
}: {
|
||||||
emoji: Emoji;
|
emoji: Emoji;
|
||||||
|
network: Network | null;
|
||||||
|
nameOnNetwork: string | null;
|
||||||
variants: Emoji[];
|
variants: Emoji[];
|
||||||
base: Emoji | null;
|
base: Emoji | null;
|
||||||
onSelect: (key: string) => void;
|
onSelect: (key: string) => void;
|
||||||
|
|
@ -553,6 +788,21 @@ function Detail({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{network ? (
|
||||||
|
<p className={styles.onNetwork}>
|
||||||
|
On {network.name}:{" "}
|
||||||
|
{nameOnNetwork ? <code>:{nameOnNetwork}:</code> : KIND_LABEL[network.kind].toLowerCase()}
|
||||||
|
{(() => {
|
||||||
|
const packs = network.packs.filter((p) => p.keys.includes(emoji.key)).map((p) => p.title);
|
||||||
|
return packs.length ? <span> · in {packs.join(", ")}</span> : <span> · not in a bundle for this network</span>;
|
||||||
|
})()}
|
||||||
|
</p>
|
||||||
|
) : emoji.shortcodes?.[0] ? (
|
||||||
|
<p className={styles.onNetwork}>
|
||||||
|
Shortcode <code>:{emoji.shortcodes[0]}:</code>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<button type="button" className={styles.copy} data-copy={emoji.char}>
|
<button type="button" className={styles.copy} data-copy={emoji.char}>
|
||||||
Copy emoji
|
Copy emoji
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue