From 7242352884fdb806158678ae4fcff7675d4f3aaf Mon Sep 17 00:00:00 2001 From: Danial Raza Date: Thu, 10 Jul 2025 23:46:25 +0200 Subject: [PATCH] feat!: role gradient colors (#10961) * feat: role gradient colors * refactor: `options.colors && { ... }` * refactor(Role)!: remove `color` * feat: `RoleColorsResolvable` * feat(Constants): add `HolographicStyle` --- .../src/managers/GuildMemberRoleManager.js | 2 +- .../discord.js/src/managers/RoleManager.js | 48 ++++++++++++++--- packages/discord.js/src/structures/Role.js | 53 ++++++++++++++----- packages/discord.js/src/util/Constants.js | 16 ++++++ packages/discord.js/typings/index.d.ts | 23 ++++++-- 5 files changed, 118 insertions(+), 24 deletions(-) diff --git a/packages/discord.js/src/managers/GuildMemberRoleManager.js b/packages/discord.js/src/managers/GuildMemberRoleManager.js index 0454d5f98..609ed43ed 100644 --- a/packages/discord.js/src/managers/GuildMemberRoleManager.js +++ b/packages/discord.js/src/managers/GuildMemberRoleManager.js @@ -72,7 +72,7 @@ class GuildMemberRoleManager extends DataManager { * @readonly */ get color() { - const coloredRoles = this.cache.filter(role => role.color); + const coloredRoles = this.cache.filter(role => role.colors.primaryColor); if (!coloredRoles.size) return null; return coloredRoles.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev)); } diff --git a/packages/discord.js/src/managers/RoleManager.js b/packages/discord.js/src/managers/RoleManager.js index d2293e947..1f741b3d6 100644 --- a/packages/discord.js/src/managers/RoleManager.js +++ b/packages/discord.js/src/managers/RoleManager.js @@ -109,12 +109,22 @@ class RoleManager extends CachedManager { * @returns {?Snowflake} */ + /** + * @typedef {Object} RoleColorsResolvable + * @property {ColorResolvable} primaryColor The primary color of the role + * @property {ColorResolvable} [secondaryColor] The secondary color of the role. + * This will make the role a gradient between the other provided colors + * @property {ColorResolvable} [tertiaryColor] The tertiary color of the role. + * When sending `tertiaryColor` the API enforces the role color to be a holographic style with values of `primaryColor = 11127295`, `secondaryColor = 16759788`, and `tertiaryColor = 16761760`. + * These values are available as a constant: `Constants.HolographicStyle` + */ + /** * Options used to create a new role. * * @typedef {Object} RoleCreateOptions * @property {string} [name] The name of the new role - * @property {ColorResolvable} [color] The data to create the role with + * @property {RoleColorsResolvable} [colors] The colors to create the role with * @property {boolean} [hoist] Whether or not the new role should be hoisted * @property {PermissionResolvable} [permissions] The permissions for the new role * @property {number} [position] The position of the new role @@ -141,16 +151,30 @@ class RoleManager extends CachedManager { * // Create a new role with data and a reason * guild.roles.create({ * name: 'Super Cool Blue People', - * color: Colors.Blue, * reason: 'we needed a role for Super Cool People', + * colors: { + * primaryColor: Colors.Blue, + * }, + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Create a role with holographic colors + * guild.roles.create({ + * name: 'Holographic Role', + * reason: 'Creating a role with holographic effect', + * colors: { + * primaryColor: Constants.HolographicStyle.Primary, + * secondaryColor: Constants.HolographicStyle.Secondary, + * tertiaryColor: Constants.HolographicStyle.Tertiary, + * }, * }) * .then(console.log) * .catch(console.error); */ async create(options = {}) { - let { color, permissions, icon } = options; + let { permissions, icon } = options; const { name, hoist, position, mentionable, reason, unicodeEmoji } = options; - color &&= resolveColor(color); if (permissions !== undefined) permissions = new PermissionsBitField(permissions); if (icon) { const guildEmojiURL = this.guild.emojis.resolve(icon)?.imageURL(); @@ -158,10 +182,16 @@ class RoleManager extends CachedManager { if (typeof icon !== 'string') icon = undefined; } + const colors = options.colors && { + primary_color: resolveColor(options.colors.primaryColor), + secondary_color: options.colors.secondaryColor && resolveColor(options.colors.secondaryColor), + tertiary_color: options.colors.tertiaryColor && resolveColor(options.colors.tertiaryColor), + }; + const data = await this.client.rest.post(Routes.guildRoles(this.guild.id), { body: { name, - color, + colors, hoist, permissions, mentionable, @@ -212,9 +242,15 @@ class RoleManager extends CachedManager { if (typeof icon !== 'string') icon = undefined; } + const colors = options.colors && { + primary_color: resolveColor(options.colors.primaryColor), + secondary_color: options.colors.secondaryColor && resolveColor(options.colors.secondaryColor), + tertiary_color: options.colors.tertiaryColor && resolveColor(options.colors.tertiaryColor), + }; + const body = { name: options.name, - color: options.color === undefined ? undefined : resolveColor(options.color), + colors, hoist: options.hoist, permissions: options.permissions === undefined ? undefined : new PermissionsBitField(options.permissions), mentionable: options.mentionable, diff --git a/packages/discord.js/src/structures/Role.js b/packages/discord.js/src/structures/Role.js index 982de0f0d..92ee08b3f 100644 --- a/packages/discord.js/src/structures/Role.js +++ b/packages/discord.js/src/structures/Role.js @@ -57,13 +57,27 @@ class Role extends Base { this.name = data.name; } - if ('color' in data) { + /** + * @typedef {Object} RoleColors + * @property {number} primaryColor The primary color of the role + * @property {?number} secondaryColor The secondary color of the role. + * This will make the role a gradient between the other provided colors + * @property {?number} tertiaryColor The tertiary color of the role. + * When sending `tertiaryColor` the API enforces the role color to be a holographic style with values of `primaryColor = 11127295`, `secondaryColor = 16759788`, and `tertiaryColor = 16761760`. + * These values are available as a constant: `Constants.HolographicStyle` + */ + + if ('colors' in data) { /** - * The base 10 color of the role + * The colors of the role * - * @type {number} + * @type {RoleColors} */ - this.color = data.color; + this.colors = { + primaryColor: data.colors.primary_color, + secondaryColor: data.colors.secondary_color, + tertiaryColor: data.colors.tertiary_color, + }; } if ('hoist' in data) { @@ -257,7 +271,7 @@ class Role extends Base { * * @typedef {Object} RoleData * @property {string} [name] The name of the role - * @property {ColorResolvable} [color] The color of the role, either a hex string or a base 10 number + * @property {RoleColorsResolvable} [colors] The colors of the role * @property {boolean} [hoist] Whether or not the role should be hoisted * @property {number} [position] The position of the role * @property {PermissionResolvable} [permissions] The permissions of the role @@ -315,19 +329,28 @@ class Role extends Base { } /** - * Sets a new color for the role. + * Sets new colors for the role. * - * @param {ColorResolvable} color The color of the role - * @param {string} [reason] Reason for changing the role's color + * @param {RoleColorsResolvable} colors The colors of the role + * @param {string} [reason] Reason for changing the role's colors * @returns {Promise} * @example - * // Set the color of a role - * role.setColor('#FF0000') - * .then(updated => console.log(`Set color of role to ${updated.color}`)) + * // Set the colors of a role + * role.setColors({ primaryColor: '#FF0000', secondaryColor: '#00FF00', tertiaryColor: '#0000FF' }) + * .then(updated => console.log(`Set colors of role to ${updated.colors}`)) + * .catch(console.error); + * @example + * // Set holographic colors using constants + * role.setColors({ + * primaryColor: Constants.HolographicStyle.Primary, + * secondaryColor: Constants.HolographicStyle.Secondary, + * tertiaryColor: Constants.HolographicStyle.Tertiary, + * }) + * .then(updated => console.log(`Set holographic colors for role ${updated.name}`)) * .catch(console.error); */ - async setColor(color, reason) { - return this.edit({ color, reason }); + async setColors(colors, reason) { + return this.edit({ colors, reason }); } /** @@ -475,7 +498,9 @@ class Role extends Base { role && this.id === role.id && this.name === role.name && - this.color === role.color && + this.colors.primaryColor === role.colors.primaryColor && + this.colors.secondaryColor === role.colors.secondaryColor && + this.colors.tertiaryColor === role.colors.tertiaryColor && this.hoist === role.hoist && this.position === role.position && this.permissions.bitfield === role.permissions.bitfield && diff --git a/packages/discord.js/src/util/Constants.js b/packages/discord.js/src/util/Constants.js index e9d856dec..4d5d4dd45 100644 --- a/packages/discord.js/src/util/Constants.js +++ b/packages/discord.js/src/util/Constants.js @@ -222,6 +222,21 @@ exports.StickerFormatExtensionMap = { [StickerFormatType.GIF]: ImageFormat.GIF, }; +/** + * Holographic color values for role styling. + * When using `tertiaryColor`, the API enforces these specific values for holographic effect. + * + * @typedef {Object} HolographicStyle + * @property {number} Primary 11127295 (0xA9FFFF) + * @property {number} Secondary 16759788 (0xFFCCCC) + * @property {number} Tertiary 16761760 (0xFFE0A0) + */ +exports.HolographicStyle = { + Primary: 11_127_295, + Secondary: 16_759_788, + Tertiary: 16_761_760, +}; + /** * @typedef {Object} Constants Constants that can be used in an enum or object-like way. * @property {number} MaxBulkDeletableMessageAge Max bulk deletable message age @@ -232,4 +247,5 @@ exports.StickerFormatExtensionMap = { * @property {VoiceBasedChannelTypes} VoiceBasedChannelTypes The types of channels that are voice-based * @property {SelectMenuTypes} SelectMenuTypes The types of components that are select menus. * @property {Object} StickerFormatExtensionMap A mapping between sticker formats and their respective image formats. + * @property {HolographicStyle} HolographicStyle Holographic color values for role styling. */ diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 3361fb57a..3a786889f 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -2823,9 +2823,21 @@ export class RichPresenceAssets { public smallImageURL(options?: ImageURLOptions): string | null; } +export interface RoleColors { + primaryColor: number; + secondaryColor: number | null; + tertiaryColor: number | null; +} + +export interface RoleColorsResolvable { + primaryColor: ColorResolvable; + secondaryColor?: ColorResolvable; + tertiaryColor?: ColorResolvable; +} + export class Role extends Base { private constructor(client: Client, data: APIRole, guild: Guild); - public color: number; + public colors: RoleColors; public get createdAt(): Date; public get createdTimestamp(): number; public get editable(): boolean; @@ -2853,7 +2865,7 @@ export class Role extends Base { channel: NonThreadGuildBasedChannel | Snowflake, checkAdmin?: boolean, ): Readonly; - public setColor(color: ColorResolvable, reason?: string): Promise; + public setColors(colors: RoleColorsResolvable, reason?: string): Promise; public setHoist(hoist?: boolean, reason?: string): Promise; public setMentionable(mentionable?: boolean, reason?: string): Promise; public setName(name: string, reason?: string): Promise; @@ -3808,6 +3820,11 @@ export type UndeletableMessageType = export const Constants: { GuildTextBasedChannelTypes: GuildTextBasedChannelTypes[]; + HolographicStyle: { + Primary: 11_127_295; + Secondary: 16_759_788; + Tertiary: 16_761_760; + }; MaxBulkDeletableMessageAge: 1_209_600_000; NonSystemMessageTypes: NonSystemMessageType[]; SelectMenuTypes: SelectMenuType[]; @@ -6836,7 +6853,7 @@ export interface ResolvedOverwriteOptions { } export interface RoleData { - color?: ColorResolvable; + colors?: RoleColorsResolvable; hoist?: boolean; icon?: Base64Resolvable | BufferResolvable | EmojiResolvable | null; mentionable?: boolean;