From c1be1f4a43784f43dcde016fa1d807ef796e6f0a Mon Sep 17 00:00:00 2001 From: Alexander Khrushkov Date: Wed, 5 Aug 2026 23:12:42 +0300 Subject: [PATCH] feat(announcements): add a discord icon key for chat-sourced announcements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An announcement imported from a Discord channel had nothing that said so at a glance — it rendered under whatever glyph its type defaults to, indistinguishable from one an admin typed into the composer. lucide-react carries no brand marks (they were split out of the icon set upstream), so the glyph is drawn in this package against lucide own component contract: same props, same ref target, currentColor by default. Call sites keep reading ANNOUNCEMENT_ICONS and know nothing about the difference. Consumers on an older build of this package are unaffected: an icon key they do not recognise already falls back to the type default rather than rendering an empty square (iconForAnnouncement). --- src/components/announcements/DiscordIcon.tsx | 42 +++++++++++++++++++ .../announcements/__tests__/icons.test.ts | 10 +++++ src/components/announcements/icons.ts | 9 +++- 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/components/announcements/DiscordIcon.tsx diff --git a/src/components/announcements/DiscordIcon.tsx b/src/components/announcements/DiscordIcon.tsx new file mode 100644 index 0000000..c4946a4 --- /dev/null +++ b/src/components/announcements/DiscordIcon.tsx @@ -0,0 +1,42 @@ +import { forwardRef } from 'react'; +import type { LucideProps } from 'lucide-react'; + +/** + * Discord's brand mark, drawn here because lucide-react carries no brand + * glyphs at all (they were split out of the icon set upstream) and an + * announcement that arrived from a Discord channel has nothing else that + * says so at a glance. + * + * Deliberately shaped to lucide's own component contract — same props, same + * ref target, `currentColor` by default — so it can sit in + * `ANNOUNCEMENT_ICONS` beside real lucide icons and be rendered by call + * sites that know nothing about it (``). + * + * Filled rather than stroked, unlike every glyph around it: a brand mark + * redrawn as outlines stops reading as that brand. `fill={color}` keeps it + * inheriting colour exactly the way the stroked icons do, so it still + * follows `--text-muted` / `--accent` with no special casing. + */ +export const DiscordIcon = forwardRef(function DiscordIcon( + { size = 24, color = 'currentColor', className, style, ...rest }, + ref, +) { + return ( + + ); +}); diff --git a/src/components/announcements/__tests__/icons.test.ts b/src/components/announcements/__tests__/icons.test.ts index 0ef7485..1feedd1 100644 --- a/src/components/announcements/__tests__/icons.test.ts +++ b/src/components/announcements/__tests__/icons.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest'; import { ANNOUNCEMENT_ICON_KEYS, ANNOUNCEMENT_ICONS, iconForAnnouncement } from '../icons.js'; +import { DiscordIcon } from '../DiscordIcon.js'; import type { AnnouncementType } from '../types.js'; // types.ts exports `AnnouncementType` as a union only, no runtime array — this @@ -12,9 +13,18 @@ describe('ANNOUNCEMENT_ICON_KEYS / ANNOUNCEMENT_ICONS', () => { expect([...ANNOUNCEMENT_ICON_KEYS]).toEqual([ 'release', 'update', 'event', 'maintenance', 'security', 'megaphone', 'gift', 'sparkles', 'alert-triangle', 'info', 'key', 'coins', 'party', + 'discord', ]); }); + // The only key whose glyph is not a lucide icon — it is drawn in this + // package (lucide ships no brand marks). Pinned so a refactor that loses + // the local component and silently leaves the key mapped to nothing gets + // caught here rather than as an empty square in three portals. + it('maps the discord key to the locally drawn brand mark', () => { + expect(ANNOUNCEMENT_ICONS.discord).toBe(DiscordIcon); + }); + it('has exactly one glyph per key, with no gaps', () => { for (const key of ANNOUNCEMENT_ICON_KEYS) { expect(ANNOUNCEMENT_ICONS[key]).toBeTruthy(); diff --git a/src/components/announcements/icons.ts b/src/components/announcements/icons.ts index dbf3844..f44d53d 100644 --- a/src/components/announcements/icons.ts +++ b/src/components/announcements/icons.ts @@ -3,6 +3,7 @@ import { Megaphone, Gift, Sparkles, AlertTriangle, Info, Key, Coins, PartyPopper, } from 'lucide-react'; import type { AnnouncementType } from './types.js'; +import { DiscordIcon } from './DiscordIcon.js'; /** * The closed set of icon keys an announcement can be tagged with. A stored @@ -25,14 +26,19 @@ import type { AnnouncementType } from './types.js'; export const ANNOUNCEMENT_ICON_KEYS = [ 'release', 'update', 'event', 'maintenance', 'security', 'megaphone', 'gift', 'sparkles', 'alert-triangle', 'info', 'key', 'coins', 'party', + 'discord', ] as const; export type AnnouncementIconKey = typeof ANNOUNCEMENT_ICON_KEYS[number]; /** - * One lucide-react component per key. Every consumer — this row, and + * One icon component per key. Every consumer — this row, and * sphere-backoffice's icon picker — reads a glyph from this single map, so * there is exactly one place that decides what each key looks like. + * + * All lucide-react except `discord`: lucide carries no brand marks, so that + * one is drawn locally against the same component contract (see + * DiscordIcon.tsx) and is otherwise indistinguishable to every call site. */ export const ANNOUNCEMENT_ICONS: Record = { release: Rocket, @@ -48,6 +54,7 @@ export const ANNOUNCEMENT_ICONS: Record = { key: Key, coins: Coins, party: PartyPopper, + discord: DiscordIcon, }; function isAnnouncementIconKey(value: string): value is AnnouncementIconKey {