Add icon support and a richer color picker to Custom User Tags.

This commit is contained in:
Rosie Val 2026-08-08 00:25:44 -04:00
parent 3341c96f02
commit 10e9b0d815
15 changed files with 536 additions and 25 deletions

View file

@ -1,6 +1,6 @@
{
"name": "Custom User Tags",
"description": "Create your own tags (text + color) and assign them to specific users - long-press a name in the member list or profile popout to manage one, or add one by user ID in settings.",
"description": "Create your own tags (text, color, and icon) and assign them to specific users - long-press a name in the member list or profile popout to manage one, or add one by user ID in settings. Includes a color picker with hex/RGB support and a library of custom icons.",
"authors": [
{ "name": "everestmcarthur" }
],

View file

@ -0,0 +1,82 @@
export interface IconDef {
id: string;
name: string;
path: string;
fallback: string;
viewBox?: string;
}
export const ICONS: IconDef[] = [
{ id: "none", name: "No icon", path: "", fallback: "" },
{ id: "star", name: "Star", path: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z", fallback: "★" },
{ id: "heart", name: "Heart", path: "M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z", fallback: "♥" },
{ id: "crown", name: "Crown", path: "M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11H5zm14 3c0 .6-.4 1-1 1H6c-.6 0-1-.4-1-1v-1h14v1z", fallback: "♛" },
{ id: "gem", name: "Gem", path: "M12 2l8 5-8 15-8-15 8-5z", fallback: "💎" },
{ id: "shield", name: "Shield", path: "M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z", fallback: "🛡" },
{ id: "bolt", name: "Bolt", path: "M11 21h-1l1-7H7.5c-.58 0-.57-.32-.38-.66.19-.34.05-.08.07-.12C8.48 10.94 11.07 6 13.5 6c.46 0 .85.33.96.79L15 10h3.5c.49 0 .64.32.45.66-.19.34-.05.08-.07.12C17.52 14.06 15.07 19 13 19l-1 2z", fallback: "⚡" },
{ id: "flame", name: "Flame", path: "M13.5.67s.74 2.65.74 4.8c0 2.06-1.35 3.73-3.41 3.73-2.07 0-3.63-1.67-3.63-3.73 0-2.15.74-4.8.74-4.8C5.26 3.12 2 7.05 2 12c0 5.52 4.48 10 10 10s10-4.48 10-10c0-4.95-3.26-8.88-7.5-11.33z", fallback: "🔥" },
{ id: "check", name: "Check", path: "M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z", fallback: "✓" },
{ id: "cross", name: "Cross", path: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z", fallback: "✕" },
{ id: "bell", name: "Bell", path: "M18 16v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z", fallback: "🔔" },
{ id: "pin", name: "Pin", path: "M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z", fallback: "📌" },
{ id: "lock", name: "Lock", path: "M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z", fallback: "🔒" },
{ id: "key", name: "Key", path: "M12.65 10a5.998 5.998 0 0 0-6.88-3.88c-2.29.46-4.15 2.29-4.63 4.58A6.006 6.006 0 0 0 7 18a5.99 5.99 0 0 0 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z", fallback: "🔑" },
{ id: "eye", name: "Eye", path: "M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z", fallback: "👁" },
{ id: "flag", name: "Flag", path: "M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6z", fallback: "🚩" },
{ id: "tag", name: "Tag", path: "M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z", fallback: "🏷" },
{ id: "badge", name: "Badge", path: "M20.5 6c-2.61.7-5.67 1-8.5 1s-5.89-.3-8.5-1L3 8c1.86.5 4 .83 6 1v13h2v-6h2v6h2V9c2-.17 4.14-.5 6-1l-.5-2zM12 6c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z", fallback: "🎖" },
{ id: "robot", name: "Robot", path: "M20 9V7c0-1.1-.9-2-2-2h-3.2c-.5-1.9-2.2-3.3-4.2-3.3S7.9 3.1 7.4 5H4.2C3 5 2 6 2 7.2v9.6C2 18 3 19 4.2 19h.2v1c0 .6.4 1 1 1h1c.6 0 1-.4 1-1v-1h8v1c0 .6.4 1 1 1h1c.6 0 1-.4 1-1v-1h.2c1.2 0 2.2-1 2.2-2.2V9h-2zm-4 7H8v-2h8v2zm0-4H8V9h8v3z", fallback: "🤖" },
{ id: "moon", name: "Moon", path: "M9 2c-1.05 0-2.05.16-3 .46 1.69 1.23 2.8 3.24 2.8 5.54 0 3.87-3.13 7-7 7-1.04 0-2.02-.23-2.9-.64C2.24 18.24 6.36 22 11.5 22c5.5 0 10-4.5 10-10S17 2 11.5 2c-.83 0-1.63.1-2.39.29C9.32 2.1 9.16 2 9 2z", fallback: "🌙" },
{ id: "sun", name: "Sun", path: "M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.79 1.42-1.41zM4 10.5H1v2h3v-2zm13-6.9l-1.41 1.41 1.79 1.79 1.42-1.41-1.8-1.79zM20 10.5v2h3v-2h-3zm-8-5c-3.87 0-7 3.13-7 7s3.13 7 7 7 7-3.13 7-7-3.13-7-7-7zm0 12c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm-4 6.91v3h2v-3h-2zm6 0h-2v3h2v-3z", fallback: "☀" },
{ id: "cloud", name: "Cloud", path: "M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z", fallback: "☁" },
{ id: "music", name: "Music", path: "M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z", fallback: "🎵" },
{ id: "mic", name: "Mic", path: "M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zM17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z", fallback: "🎤" },
{ id: "gamepad", name: "Gamepad", path: "M21 6H3c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-10 7H8v3H6v-3H3v-2h3V8h2v3h3v2zm4.5 2c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4-3c-.83 0-1.5-.67-1.5-1.5S18.67 9 19.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z", fallback: "🎮" },
{ id: "diamond", name: "Diamond", path: "M19 3H5L2 9l10 12L22 9l-3-6zM7.43 9.14 9.18 5h2.14l1.75 4.14H7.43zm5.47 0h3.9l1.75-4.14h-2.14l-1.75 4.14zM12 17.5l-3.14-4.36h6.28L12 17.5z", fallback: "♦" },
{ id: "spade", name: "Spade", path: "M12 2C9 7 4 9.91 4 14c0 2.76 1.79 5.02 4.28 5.83L7 23h10l-1.28-3.17C18.21 19.02 20 16.76 20 14c0-4.09-5-7-8-12z", fallback: "♠" },
{ id: "anchor", name: "Anchor", path: "M19 13v6c0 1.1-.9 2-2 2H7c-1.1 0-2-.9-2-2v-6c0-1.1.9-2 2-2h2.2c.55-1.84 2.2-3.15 4.05-3.19.23-1.22 1.12-2.22 2.3-2.5V3h2v2.11c1.18.28 2.07 1.28 2.3 2.5 1.85.04 3.5 1.35 4.05 3.19H17c1.1 0 2 .9 2 2z", fallback: "⚓" },
{ id: "feather", name: "Feather", path: "M20.41 3c-2.04 0-3.92.64-5.48 1.72-.94.66-1.72 1.47-2.32 2.4C11.92 6.41 10.56 6 9.12 6c-2.97 0-5.69 1.66-7.05 4.3l13.93 13.93C18.64 22.59 20.3 19.87 20.3 17c0-1.44-.41-2.8-1.12-3.95.93-.6 1.74-1.38 2.4-2.32C22.66 9.85 23.31 7.97 23.31 6c0-.33-.02-.65-.06-.97l-2.84-2.03z", fallback: "🪶" },
{ id: "leaf", name: "Leaf", path: "M17 8C8 10 5.9 16.17 3.82 21.34l1.89.66.95-2.3c.48.17.98.3 1.34.3C19 20 22 3 22 3c-1 2-3 5-5 5z", fallback: "🍃" },
{ id: "flower", name: "Flower", path: "M12 22c4.97 0 9-4.03 9-9-4.97 0-9-4.03-9-9-4.97 0-9 4.03-9 9 0 4.97 4.03 9 9 9zm0-16c3.87 0 7 3.13 7 7s-3.13 7-7 7-7-3.13-7-7 3.13-7 7-7z", fallback: "❀" },
{ id: "snowflake", name: "Snowflake", path: "M22 11h-2.65l1.76-1.76-1.41-1.41L17.24 9.88V7h-2v2.88l-2.47-2.47-1.41 1.41L13.35 11H11V8.35l1.76-1.76-1.41-1.41L9.88 6.76V4h-2v2.76L5.41 4.18 4 5.59 5.76 7.35H3v2h2.65l-1.76 1.76 1.41 1.41L6.76 11.12V14h2v-2.88l2.47 2.47 1.41-1.41L10.65 11H13v2.65l-1.76 1.76 1.41 1.41 2.47-2.47V14h2v-2.88l2.47 2.47 1.41-1.41L19.35 13H22v-2z", fallback: "❄" },
{ id: "zap", name: "Zap", path: "M11 21h-1l1-7H7.5c-.58 0-.57-.32-.38-.66.19-.34.05-.08.07-.12C8.48 10.94 11.07 6 13.5 6c.46 0 .85.33.96.79L15 10h3.5c.49 0 .64.32.45.66-.19.34-.05.08-.07.12C17.52 14.06 15.07 19 13 19l-1 2z", fallback: "⚡" },
{ id: "planet", name: "Planet", path: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z", fallback: "🪐" },
{ id: "rocket", name: "Rocket", path: "M12 2.5s-4.5 2-6.5 6c-2 4-1 9-1 9s3.5-1 5.5-3v4c0 1.66 1.34 3 3 3s3-1.34 3-3v-4c2 2 5.5 3 5.5 3s1-5-1-9c-2-4-6.5-6-6.5-6zm0 3c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z", fallback: "🚀" },
{ id: "ghost", name: "Ghost", path: "M12 2C7.58 2 4 5.58 4 10v11c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-4h10v4c0 .55.45 1 1 1h1c.55 0 1-.45 1-1V10c0-4.42-3.58-8-8-8zm0 3c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z", fallback: "👻" },
{ id: "skull", name: "Skull", path: "M12 2c-4.42 0-8 3.58-8 8 0 2.64 1.28 4.99 3.27 6.46L8 21h8l.73-4.54C18.72 14.99 20 12.64 20 10c0-4.42-3.58-8-8-8z", fallback: "💀" },
{ id: "paw", name: "Paw", path: "M17.5 10.5c.83 0 1.5-.67 1.5-1.5S18.33 7.5 17.5 7.5 16 8.17 16 9s.67 1.5 1.5 1.5zm-11 0c.83 0 1.5-.67 1.5-1.5S7.33 7.5 6.5 7.5 5 8.17 5 9s.67 1.5 1.5 1.5zm5.5 4c-2.28 0-4.32 1.23-5.42 3.05.73 1.34 2.15 2.25 3.79 2.25 1.45 0 2.73-.75 3.48-1.89.37.58.83 1.09 1.37 1.5 1.02.76 2.27 1.18 3.56 1.18 1.46 0 2.8-.52 3.85-1.38C23.2 16.6 20.5 14.5 17.5 14.5c-.83 0-1.61.18-2.32.49-.73-.32-1.53-.49-2.38-.49z", fallback: "🐾" },
{ id: "pizza", name: "Pizza", path: "M12 2C8.43 2 5.23 3.54 3.1 6.04 5.34 15.38 12 22 12 22s6.66-6.62 8.9-15.96C18.77 3.54 15.57 2 12 2zm0 15c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm0-5c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z", fallback: "🍕" },
{ id: "coffee", name: "Coffee", path: "M18.5 3H6c-1.1 0-2 .9-2 2v5.71c0 3.83 2.95 7.04 6.78 7.29 3.96.26 7.22-3.06 7.22-7v-1h.5c1.93 0 3.5-1.57 3.5-3.5S20.43 3 18.5 3z", fallback: "☕" },
{ id: "cake", name: "Cake", path: "M19 5h-2V3H7v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-7 9c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z", fallback: "🎂" },
{ id: "gift", name: "Gift", path: "M20 6h-2.18c.11-.31.18-.65.18-1 0-1.66-1.34-3-3-3-1.05 0-1.96.63-2.4 1.5-.44-.87-1.35-1.5-2.4-1.5-1.66 0-3 1.34-3 3 0 .35.07.69.18 1H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-5-2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm-7 2h14v2H8V6zm0 4h14v8H8v-8z", fallback: "🎁" },
{ id: "trophy", name: "Trophy", path: "M19 5h-2V3H7v2H5c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h2v2H7v2h10v-2h-2v-2h2c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2z", fallback: "🏆" },
{ id: "medal", name: "Medal", path: "M20 2H4c-1.1 0-2 .9-2 2v4c0 1.1.9 2 2 2h2v10h2V10h8v10h2V10h2c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM4 6h4v2H4V6zm16 0h-4v2h4V6z", fallback: "🏅" },
{ id: "lightbulb", name: "Lightbulb", path: "M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6C7.8 12.16 7 10.63 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z", fallback: "💡" },
{ id: "battery", name: "Battery", path: "M15.67 4H14V2h-4v2H8.33C7.6 4 7 4.6 7 5.33v15.33C7 21.4 7.6 22 8.33 22h7.33c.74 0 1.34-.6 1.34-1.33V5.33C17 4.6 16.4 4 15.67 4z", fallback: "🔋" },
{ id: "wifi", name: "Wifi", path: "M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3a4.237 4.237 0 0 0-6 0zm-4-4l2 2a7.074 7.074 0 0 1 10 0l2-2C15.14 9.14 8.87 9.14 5 13z", fallback: "📶" },
{ id: "code", name: "Code", path: "M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z", fallback: "</>" },
{ id: "terminal", name: "Terminal", path: "M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H4V6h16v12zM7.5 15l4.5-3-4.5-3v6zM13 15h5v-2h-5v2z", fallback: "⌘" },
{ id: "bug", name: "Bug", path: "M19 8h-1.81a5.98 5.98 0 0 0-1.19-2.56l1.5-1.5-1.41-1.41-1.5 1.5A5.96 5.96 0 0 0 13 3.06V1h-2v2.06A5.96 5.96 0 0 0 8.91 4.03l-1.5-1.5-1.41 1.41 1.5 1.5A5.98 5.98 0 0 0 6.81 8H5v2h1.09c-.05.33-.09.66-.09 1v1H5v2h1v1c0 .34.04.67.09 1H5v2h1.81c1.04 2.95 3.86 5 7.19 5s6.15-2.05 7.19-5H19v-2h-1.09c.05-.33.09-.66.09-1v-1h1v-2h-1v-1c0-.34-.04-.67-.09-1H19V8zm-4 9c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z", fallback: "🐛" },
{ id: "search", name: "Search", path: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z", fallback: "🔍" },
{ id: "home", name: "Home", path: "M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z", fallback: "⌂" },
{ id: "mail", name: "Mail", path: "M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z", fallback: "✉" },
{ id: "phone", name: "Phone", path: "M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z", fallback: "📞" },
{ id: "pen", name: "Pen", path: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z", fallback: "✎" },
{ id: "trash", name: "Trash", path: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z", fallback: "🗑" },
{ id: "clock", name: "Clock", path: "M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v7l5.25 3.15.75-1.23-4.5-2.67z", fallback: "🕐" },
{ id: "calendar", name: "Calendar", path: "M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM9 10H7v2h2v-2zm0 4H7v2h2v-2zm0-8H7v2h2V6zm4 0h-2v2h2V6zm0 8h-2v2h2v-2zm0-4h-2v2h2v-2z", fallback: "📅" },
{ id: "globe", name: "Globe", path: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z", fallback: "🌐" },
{ id: "cog", name: "Cog", path: "M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.488.488 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.42h-3.84a.484.484 0 0 0-.48.42l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.488.488 0 0 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .43-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z", fallback: "⚙" },
{ id: "megaphone", name: "Megaphone", path: "M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z", fallback: "📣" },
{ id: "cart", name: "Cart", path: "M7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zM1 2v2h2l3.6 7.59-1.35 2.45C4.52 15.37 5.48 17 7 17h12v-2H7.42c-.14 0-.25-.11-.21-.26l.04-.15L8.1 13h7.45c.75 0 1.41-.41 1.75-1.03l3.58-6.49A1.003 1.003 0 0 0 20 4H5.21l-.94-2H1zm16 16c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2z", fallback: "🛒" },
{ id: "basket", name: "Basket", path: "M17.21 9l-4.38-6.56a.993.993 0 0 0-1.66 0L6.79 9H2v2h3.05l1.65 8.33A2.01 2.01 0 0 0 8.68 21h6.64c.97 0 1.81-.71 1.96-1.67L18.95 11H22V9h-4.79zM10 5.74 13.76 10H6.24L10 5.74z", fallback: "🧺" },
{ id: "umbrella", name: "Umbrella", path: "M12 2c-5.33 4.55-8 8.48-8 11.8 0 4.98 3.8 8.2 8 8.2s8-3.22 8-8.2C20 10.48 17.33 6.55 12 2zm0 18c-2.21 0-4.39-1.79-4.39-4.38 0-2.22 1.4-4.38 4.39-4.38s4.39 2.16 4.39 4.38c0 2.59-2.18 4.38-4.39 4.38z", fallback: "☂" },
{ id: "anchor2", name: "Anchor 2", path: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z", fallback: "◎" }
];
export const ICONS_BY_ID: Record<string, IconDef> = Object.fromEntries(ICONS.map((i) => [i.id, i]));
export function getIcon(id: string | undefined): IconDef | undefined {
if (!id || id === "none") return undefined;
return ICONS_BY_ID[id];
}

View file

@ -2,19 +2,29 @@ import { chroma } from "@vendetta/metro/common";
import { rawColors } from "@vendetta/ui";
import { isValidHex } from "@shared/lib/color";
import { getUserTag } from "./tags";
import { getIcon, IconDef } from "./icons";
export interface ResolvedTag {
text: string;
textColor: any;
backgroundColor: string;
icon?: IconDef;
iconColor: any;
}
export default function resolveTag(userId: string | undefined): ResolvedTag | undefined {
const tag = getUserTag(userId);
if (!tag?.text || !isValidHex(tag.color)) return undefined;
if ((!tag?.text && !tag?.icon) || !isValidHex(tag.color)) return undefined;
const backgroundColor = tag.color;
const textColor = chroma(backgroundColor).get("lab.l") < 70 ? rawColors.WHITE_500 : rawColors.BLACK_500;
const icon = getIcon(tag.icon);
return { text: tag.text, textColor, backgroundColor };
return {
text: tag.text || "",
textColor,
backgroundColor,
icon,
iconColor: textColor
};
}

View file

@ -3,6 +3,7 @@ import { storage } from "@vendetta/plugin";
export interface UserTag {
text: string;
color: string;
icon?: string;
}
export function allTags(): Record<string, UserTag> {

View file

@ -17,9 +17,15 @@ export default () => {
const tag = resolveTag(message?.author?.id);
if (!tag) return;
const tagText = tag.icon
? tag.text
? `${tag.icon.fallback} ${tag.text}`
: tag.icon.fallback
: tag.text;
return {
...ret,
tagText: tag.text,
tagText,
tagTextColor: tag.textColor ? ReactNative.processColor(chroma(tag.textColor).hex()) : undefined,
tagBackgroundColor: ReactNative.processColor(chroma(tag.backgroundColor).hex()),
tagVerified: false,

View file

@ -42,6 +42,8 @@ const rowPatch = ([{ user }]: any[], res: any) => {
text: tag.text,
textColor: tag.textColor,
backgroundColor: tag.backgroundColor,
icon: tag.icon,
iconColor: tag.iconColor,
verified: false
});
return;
@ -58,6 +60,8 @@ const rowPatch = ([{ user }]: any[], res: any) => {
text={tag.text}
textColor={tag.textColor}
backgroundColor={tag.backgroundColor}
icon={tag.icon}
iconColor={tag.iconColor}
verified={false}
/>
);

View file

@ -28,6 +28,8 @@ export default () => {
text: tag.text,
textColor: tag.textColor,
backgroundColor: tag.backgroundColor,
icon: tag.icon,
iconColor: tag.iconColor,
verified: false
};
return;
@ -43,6 +45,8 @@ export default () => {
text={tag.text}
textColor={tag.textColor}
backgroundColor={tag.backgroundColor}
icon={tag.icon}
iconColor={tag.iconColor}
verified={false}
/>
);

View file

@ -41,6 +41,8 @@ export default function patchProfile(): () => void {
text: tag.text,
textColor: tag.textColor,
backgroundColor: tag.backgroundColor,
icon: tag.icon,
iconColor: tag.iconColor,
verified: false
});
return;
@ -60,6 +62,8 @@ export default function patchProfile(): () => void {
text={tag.text}
textColor={tag.textColor}
backgroundColor={tag.backgroundColor}
icon={tag.icon}
iconColor={tag.iconColor}
verified={false}
/>
);

View file

@ -1,23 +1,37 @@
import { React, ReactNative } from "@vendetta/metro/common";
import { findByProps } from "@vendetta/metro";
import { after } from "@vendetta/patcher";
import { findInReactTree } from "@vendetta/utils";
import Icon from "../ui/Icon";
const Tag = findByProps("getBotLabel");
const { View, Text } = ReactNative;
/**
* Discord's own Tag component ignores custom text/color props unless we push them into the
* already-rendered tree ourselves - this is what actually makes our overrides visible. Identical to
* Staff Tags' own copy of this patch - fully generic, doesn't care which plugin computed the tag.
* already-rendered tree ourselves - this is what actually makes our overrides visible. Now also
* supports an `icon` prop (an IconDef from our icon library) and `iconColor` so tags can render
* with or without text.
*/
export default () => {
if (!Tag) return () => {};
return after("default", Tag, ([{ text, textColor, backgroundColor }], ret) => {
return after("default", Tag, ([{ text, textColor, backgroundColor, icon, iconColor }], ret) => {
const label = findInReactTree(ret, (c) => typeof c?.props?.children === "string");
if (!label) return;
if (text) label.props.children = text;
if (text != null) label.props.children = text;
if (textColor) label.props.style?.push?.({ color: textColor });
if (backgroundColor) ret?.props?.style?.push?.({ backgroundColor });
if (icon?.path) {
const style = label.props.style ?? {};
ret.props.children = (
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Icon icon={icon} size={12} color={iconColor ?? textColor} style={{ marginRight: text ? 4 : 0 }} />
<Text style={style}>{text != null ? text : label.props.children}</Text>
</View>
);
}
});
};

View file

@ -0,0 +1,264 @@
import { React, ReactNative } from "@vendetta/metro/common";
import { isValidHex, normalizeHex } from "@shared/lib/color";
import { resolveSemanticColorSafe } from "@shared/lib/color";
const { View, Text, TextInput, TouchableOpacity } = ReactNative;
interface ColorInputProps {
title: string;
value?: string;
placeholder?: string;
onChange: (value: string) => void;
}
const PRESETS = [
"#F23F42", "#F0B232", "#FEE75C", "#23A55A",
"#1ABC9C", "#5865F2", "#9B59B6", "#EB459E",
"#99AAB5", "#FFFFFF",
"#FF6B6B", "#FF9F43", "#FDCB6E", "#00B894",
"#00CEC9", "#0984E3", "#6C5CE7", "#FD79A8",
"#A29BFE", "#DFE6E9", "#636E72", "#2D3436",
"#E17055", "#D63031", "#74B9FF", "#A8E6CF",
"#FDFFAB", "#FFB7B2", "#FF8B94", "#C7CEEA",
"#FF0000", "#00FF00", "#0000FF", "#FFFF00",
"#00FFFF", "#FF00FF", "#000000", "#808080"
];
function clamp(n: number, min: number, max: number) {
return Math.max(min, Math.min(max, Math.round(n)));
}
function hexToRgb(hex: string): [number, number, number] {
const full = normalizeHex(hex).slice(1);
return [
parseInt(full.slice(0, 2), 16),
parseInt(full.slice(2, 4), 16),
parseInt(full.slice(4, 6), 16)
];
}
function rgbToHex(r: number, g: number, b: number): string {
return "#" + [r, g, b].map((n) => clamp(n, 0, 255).toString(16).padStart(2, "0")).join("");
}
function rgbToHsv(r: number, g: number, b: number): [number, number, number] {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
let h = 0;
if (d !== 0) {
if (max === r) h = ((g - b) / d + 6) % 6;
else if (max === g) h = (b - r) / d + 2;
else h = (r - g) / d + 4;
h *= 60;
}
return [h, max === 0 ? 0 : d / max, max];
}
function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
const c = v * s;
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
const m = v - c;
let [r, g, b] = [0, 0, 0];
if (h < 60) [r, g, b] = [c, x, 0];
else if (h < 120) [r, g, b] = [x, c, 0];
else if (h < 180) [r, g, b] = [0, c, x];
else if (h < 240) [r, g, b] = [0, x, c];
else if (h < 300) [r, g, b] = [x, 0, c];
else [r, g, b] = [c, 0, x];
return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
}
function rainbow(count: number): string[] {
return Array.from({ length: count }, (_, i) => rgbToHex(...hsvToRgb((i * 360) / count, 1, 1)));
}
function shades(base: string, count: number): string[] {
const [h, s, v] = rgbToHsv(...hexToRgb(base));
return Array.from({ length: count }, (_, i) => rgbToHex(...hsvToRgb(h, s, 0.2 + (0.8 * i) / (count - 1))));
}
function randomHex(): string {
return rgbToHex(Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), Math.floor(Math.random() * 256));
}
export default function ColorInput({ title, value, placeholder, onChange }: ColorInputProps) {
const normalized = value ? normalizeHex(value) : "";
const valid = !value || isValidHex(value);
const swatchColor = isValidHex(value) ? normalized : (isValidHex(placeholder) ? normalizeHex(placeholder) : "#5865F2");
const [r, g, b] = hexToRgb(swatchColor);
const textColor = resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1");
const updateHex = (text: string) => {
const t = text.trim().toLowerCase();
if (!t) {
onChange("");
return;
}
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(t)) {
onChange(t.toUpperCase());
} else {
onChange(text);
}
};
const updateRgb = (component: "r" | "g" | "b", raw: string) => {
const num = parseInt(raw, 10);
if (Number.isNaN(num)) return;
const next = [r, g, b];
if (component === "r") next[0] = num;
else if (component === "g") next[1] = num;
else next[2] = num;
onChange(rgbToHex(...next as [number, number, number]));
};
const hueColors = rainbow(12);
const shadeColors = shades(swatchColor, 6);
return (
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<View style={{ flexDirection: "row", alignItems: "center", marginBottom: 8 }}>
<View
style={{
width: 28,
height: 28,
borderRadius: 6,
backgroundColor: swatchColor,
marginRight: 10,
borderWidth: 1,
borderColor: "rgba(128,128,128,0.35)"
}}
/>
<Text style={{ fontSize: 15, fontWeight: "600", color: textColor }}>{title}</Text>
</View>
<TextInput
value={value ?? ""}
placeholder={placeholder ?? "#5865F2"}
onChangeText={updateHex}
autoCapitalize="none"
autoCorrect={false}
placeholderTextColor="rgba(128,128,128,0.6)"
style={{
borderWidth: 1,
borderColor: valid ? "rgba(128,128,128,0.35)" : "#F23F42",
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 8,
fontSize: 14,
marginBottom: 6,
color: textColor
}}
/>
{valid && (
<View style={{ flexDirection: "row", justifyContent: "space-between", marginBottom: 8 }}>
{(["r", "g", "b"] as const).map((c, i) => (
<View key={c} style={{ flex: 1, marginHorizontal: c === "g" ? 8 : 0 }}>
<Text style={{ fontSize: 11, color: textColor, opacity: 0.7, textTransform: "uppercase", marginBottom: 2 }}>{c}</Text>
<TextInput
value={String([r, g, b][i])}
keyboardType="numeric"
onChangeText={(v: string) => updateRgb(c, v)}
style={{
borderWidth: 1,
borderColor: "rgba(128,128,128,0.35)",
borderRadius: 6,
paddingHorizontal: 8,
paddingVertical: 6,
fontSize: 13,
color: textColor
}}
/>
</View>
))}
</View>
)}
{valid && (
<View style={{ flexDirection: "row", flexWrap: "wrap", marginBottom: 4 }}>
{shadeColors.map((color) => (
<TouchableOpacity
key={color}
onPress={() => onChange(color.toUpperCase())}
style={{
width: 28,
height: 28,
borderRadius: 6,
backgroundColor: color,
marginRight: 6,
marginBottom: 6,
borderWidth: value?.toLowerCase() === color.toLowerCase() ? 2 : 1,
borderColor: value?.toLowerCase() === color.toLowerCase() ? "#ffffff" : "rgba(128,128,128,0.35)"
}}
/>
))}
</View>
)}
{valid && (
<View style={{ flexDirection: "row", flexWrap: "wrap", marginBottom: 4 }}>
{hueColors.map((color) => (
<TouchableOpacity
key={color}
onPress={() => onChange(color.toUpperCase())}
style={{
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: color,
marginRight: 6,
marginBottom: 6,
borderWidth: value?.toLowerCase() === color.toLowerCase() ? 2 : 1,
borderColor: value?.toLowerCase() === color.toLowerCase() ? "#ffffff" : "rgba(128,128,128,0.35)"
}}
/>
))}
</View>
)}
<TouchableOpacity
onPress={() => onChange(randomHex())}
style={{
paddingVertical: 8,
paddingHorizontal: 10,
borderRadius: 8,
backgroundColor: "rgba(88,101,242,0.2)",
alignItems: "center",
marginBottom: 8
}}
>
<Text style={{ color: "#5865F2", fontWeight: "600" }}>Random color</Text>
</TouchableOpacity>
<View style={{ flexDirection: "row", flexWrap: "wrap" }}>
{PRESETS.map((preset) => {
const active = value?.toLowerCase() === preset.toLowerCase();
return (
<TouchableOpacity
key={preset}
onPress={() => onChange(preset)}
style={{
width: 24,
height: 24,
borderRadius: 6,
backgroundColor: preset,
marginRight: 8,
marginBottom: 8,
borderWidth: active ? 2 : 1,
borderColor: active ? "#ffffff" : "rgba(128,128,128,0.35)"
}}
/>
);
})}
</View>
{!valid && (
<Text style={{ color: "#F23F42", marginTop: 2, fontSize: 12 }}>
Invalid hex color, e.g. #5865F2
</Text>
)}
</View>
);
}

View file

@ -0,0 +1,24 @@
import { React } from "@vendetta/metro/common";
import { findByProps } from "@vendetta/metro";
import { IconDef } from "../lib/icons";
const svgModule = findByProps("Svg") as Record<string, any> | undefined;
const Svg = svgModule?.Svg ?? (() => null);
const Path = svgModule?.Path ?? (() => null);
interface IconProps {
icon: IconDef;
size?: number;
color?: string;
style?: any;
}
export default function Icon({ icon, size = 12, color = "#fff", style }: IconProps) {
if (!icon.path) return null;
const viewBox = icon.viewBox ?? "0 0 24 24";
return (
<Svg width={size} height={size} viewBox={viewBox} fill={color} style={style}>
<Path d={icon.path} />
</Svg>
);
}

View file

@ -0,0 +1,57 @@
import { React, ReactNative } from "@vendetta/metro/common";
import { resolveSemanticColorSafe } from "@shared/lib/color";
import { ICONS } from "../lib/icons";
import Icon from "./Icon";
const { View, Text, TouchableOpacity, ScrollView } = ReactNative;
interface IconPickerProps {
title?: string;
value?: string;
onChange: (value: string) => void;
color?: string;
}
export default function IconPicker({ title = "Icon", value, onChange, color = "#5865F2" }: IconPickerProps) {
const textColor = resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1");
const iconColor = resolveSemanticColorSafe(["TEXT_NORMAL", "TEXT_DEFAULT"], "#dbdee1");
return (
<View style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
<Text style={{ fontSize: 15, fontWeight: "600", color: textColor, marginBottom: 8 }}>{title}</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ marginBottom: 8 }}>
<View style={{ flexDirection: "row" }}>
{ICONS.map((icon) => {
const active = value === icon.id;
return (
<TouchableOpacity
key={icon.id}
onPress={() => onChange(icon.id)}
style={{
width: 40,
height: 40,
borderRadius: 8,
alignItems: "center",
justifyContent: "center",
marginRight: 8,
backgroundColor: active ? color : "rgba(128,128,128,0.15)",
borderWidth: active ? 2 : 1,
borderColor: active ? "#ffffff" : "rgba(128,128,128,0.35)"
}}
>
{icon.path ? (
<Icon icon={icon} size={20} color={active ? "#ffffff" : iconColor} />
) : (
<Text style={{ fontSize: 12, color: active ? "#ffffff" : iconColor }}>—</Text>
)}
</TouchableOpacity>
);
})}
</View>
</ScrollView>
<Text style={{ fontSize: 13, color: textColor, opacity: 0.7 }}>
{ICONS.find((i) => i.id === value)?.name ?? "No icon"}
</Text>
</View>
);
}

View file

@ -5,10 +5,12 @@ import { TableRowGroup, TextInput } from "@shared/ui/table";
import { showToast } from "@vendetta/ui/toasts";
import SettingsScaffold from "@shared/ui/SettingsScaffold";
import ListSection from "@shared/ui/ListSection";
import ColorInput from "@shared/ui/ColorInput";
import PrimaryButton from "@shared/ui/PrimaryButton";
import NoteBox from "@shared/ui/NoteBox";
import { allTags, setUserTag, removeUserTag } from "../lib/tags";
import { getIcon } from "../lib/icons";
import ColorInput from "./ColorInput";
import IconPicker from "./IconPicker";
const { View } = ReactNative;
@ -16,7 +18,8 @@ function AddTagForm() {
const [userId, setUserId] = React.useState("");
const [text, setText] = React.useState("");
const [color, setColor] = React.useState("#5865F2");
const canSave = !!userId.trim() && !!text.trim();
const [icon, setIcon] = React.useState("none");
const canSave = !!userId.trim() && (!!text.trim() || icon !== "none");
return (
<View style={{ paddingHorizontal: 16, paddingBottom: 12 }}>
@ -26,17 +29,19 @@ function AddTagForm() {
value={userId}
onChange={setUserId}
/>
<TextInput label="Tag text" placeholder="e.g. FRIEND" value={text} onChange={setText} />
<TextInput label="Tag text (optional if icon is set)" placeholder="e.g. FRIEND" value={text} onChange={setText} />
<IconPicker title="Icon" value={icon} onChange={setIcon} color={color} />
<ColorInput title="Color" value={color} onChange={setColor} />
<PrimaryButton
label="Save tag"
disabled={!canSave}
style={{ marginTop: 8 }}
onPress={() => {
setUserTag(userId.trim(), { text: text.trim(), color });
setUserTag(userId.trim(), { text: text.trim(), color, icon: icon === "none" ? undefined : icon });
showToast(`Tagged user ${userId.trim()}`, undefined);
setUserId("");
setText("");
setIcon("none");
}}
/>
</View>
@ -62,15 +67,19 @@ export default function Settings() {
<ListSection
title="Tagged users"
emptyText="No one's tagged yet."
items={userIds.map((id) => ({
key: id,
label: tags[id].text,
subLabel: `${id} • Tap to remove`,
onPress: () => {
removeUserTag(id);
showToast("Removed tag", undefined);
}
}))}
items={userIds.map((id) => {
const icon = getIcon(tags[id].icon)?.fallback;
const label = icon ? `${icon} ${tags[id].text}`.trim() : tags[id].text;
return {
key: id,
label,
subLabel: `${id} • Tap to remove`,
onPress: () => {
removeUserTag(id);
showToast("Removed tag", undefined);
}
};
})}
/>
</SettingsScaffold>
);

View file

@ -0,0 +1,29 @@
import { React, ReactNative } from "@vendetta/metro/common";
import { ResolvedTag } from "../lib/resolveTag";
import Icon from "./Icon";
const { View, Text } = ReactNative;
const baseStyle = {
flexDirection: "row",
alignItems: "center",
borderRadius: 4,
paddingHorizontal: 4,
paddingVertical: 2,
marginLeft: 4,
overflow: "hidden"
};
interface TagProps {
tag: ResolvedTag;
style?: any;
}
export default function CustomTag({ tag, style }: TagProps) {
return (
<View style={[baseStyle, { backgroundColor: tag.backgroundColor }, style]}>
{tag.icon && tag.icon.path && <Icon icon={tag.icon} size={12} color={tag.iconColor} style={{ marginRight: tag.text ? 4 : 0 }} />}
{!!tag.text && <Text style={{ color: tag.textColor, fontSize: 11, fontWeight: "700" }}>{tag.text}</Text>}
</View>
);
}

View file

@ -2,7 +2,8 @@ import { React, ReactNative } from "@vendetta/metro/common";
import { findByProps } from "@vendetta/metro";
import { TextInput } from "@shared/ui/table";
import { showToast } from "@vendetta/ui/toasts";
import ColorInput from "@shared/ui/ColorInput";
import ColorInput from "./ColorInput";
import IconPicker from "./IconPicker";
import { getUserTag, setUserTag, removeUserTag } from "../lib/tags";
const { View } = ReactNative;
@ -28,16 +29,17 @@ function TagEditor({ userId, username }: { userId: string; username: string }) {
const existing = getUserTag(userId);
const [text, setText] = React.useState(existing?.text ?? "");
const [color, setColor] = React.useState(existing?.color ?? "#5865F2");
const [icon, setIcon] = React.useState(existing?.icon ?? "none");
const { dismissAlert, AlertModal, AlertActions, AlertActionButton } = alertParts();
if (!AlertModal || !AlertActions || !AlertActionButton) return null;
const save = () => {
if (!text.trim()) {
showToast("Enter some tag text first", undefined);
if (!text.trim() && icon === "none") {
showToast("Enter some tag text or choose an icon first", undefined);
return;
}
setUserTag(userId, { text: text.trim(), color });
setUserTag(userId, { text: text.trim(), color, icon: icon === "none" ? undefined : icon });
showToast(`Tagged ${username}`, undefined);
dismissAlert?.(ALERT_KEY);
};
@ -54,6 +56,7 @@ function TagEditor({ userId, username }: { userId: string; username: string }) {
content={
<View>
<TextInput label="Tag text" placeholder="e.g. FRIEND" value={text} onChange={setText} />
<IconPicker title="Icon" value={icon} onChange={setIcon} color={color} />
<ColorInput title="Color" value={color} onChange={setColor} />
</View>
}