kira-arts 1.3.0 โ†’ 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,8 @@ A TypeScript library for generating Discord-style visual cards โ€” profiles, wel
4
4
 
5
5
  **๐Ÿ“š Full documentation, live examples, and a Playground: [documentation](https://kira-arts.chocofactory.dev/)**
6
6
 
7
+ > ๐Ÿ”ง **Heads up:** the documentation is currently hosted at `kira-arts.chocofactory.dev`. This will move to a dedicated, more formal custom domain/branding in an upcoming release โ€” the link above will be kept up to date when that happens.
8
+
7
9
  [![npm version](https://img.shields.io/npm/v/kira-arts.svg)](https://www.npmjs.com/package/kira-arts)
8
10
  [![npm downloads](https://img.shields.io/npm/dm/kira-arts.svg)](https://www.npmjs.com/package/kira-arts)
9
11
  [![install size](https://packagephobia.com/badge?p=kira-arts)](https://packagephobia.com/result?p=kira-arts)
@@ -15,7 +17,7 @@ A TypeScript library for generating Discord-style visual cards โ€” profiles, wel
15
17
  [![release](https://github.com/worddevs/kira-arts/actions/workflows/release.yml/badge.svg)](https://github.com/worddevs/kira-arts/actions/workflows/release.yml)
16
18
  [![GitHub stars](https://img.shields.io/github/stars/worddevs/kira-arts.svg?style=flat)](https://github.com/worddevs/kira-arts/stargazers)
17
19
  [![GitHub forks](https://img.shields.io/github/forks/worddevs/kira-arts.svg?style=flat)](https://github.com/worddevs/kira-arts/network/members)
18
- [![contributors](https://img.shields.io/github/contributors/worddevs/kira-arts.svg)](https://github.com/worddevs/kira-arts/graphs/contributors)
20
+ [![contributors](https://img.shields.io/badge/contributors-2-orange)](https://github.com/worddevs/kira-arts/graphs/contributors)
19
21
  [![last commit](https://img.shields.io/github/last-commit/worddevs/kira-arts.svg)](https://github.com/worddevs/kira-arts/commits/main)
20
22
  [![open issues](https://img.shields.io/github/issues/worddevs/kira-arts.svg)](https://github.com/worddevs/kira-arts/issues)
21
23
  [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md)
@@ -25,7 +27,7 @@ A TypeScript library for generating Discord-style visual cards โ€” profiles, wel
25
27
 
26
28
  - ๐Ÿ–ผ๏ธ Profile, Welcome/Leave, Level Up, Achievement, Leaderboard, Ship (compatibility), and Now Playing cards
27
29
  - ๐ŸŽต Now Playing card ships with adapters for moonlink.js, Lavalink-based clients, discord-player, and distube
28
- - ๐ŸŽจ 8 built-in themes, Nitro/role-color aware borders
30
+ - ๐ŸŽจ 8 built-in themes, Nitro/role-color aware borders, and up to 4-color custom gradients
29
31
  - ๐Ÿงพ Output as `png`, `jpeg`, or `webp`, ready to use as a discord.js `AttachmentBuilder`
30
32
 
31
33
  ## ๐Ÿ“ฆ Installation
@@ -47,24 +49,71 @@ import { setClient, profileImage, toAttachment } from "kira-arts";
47
49
 
48
50
  const client = new Client({ intents: [GatewayIntentBits.Guilds] });
49
51
 
50
- client.once("ready", () => {
52
+ client.once("clientReady", () => {
51
53
  setClient(client); // ๐Ÿ‘ˆ required before generating any card
52
54
  });
53
55
 
54
- client.login(process.env.TOKEN);
56
+ client.on("interactionCreate", async (interaction) => {
57
+ if (interaction.isChatInputCommand()) return;
58
+
59
+ if (interaction.commandName === "card") {
60
+ const buffer = await profileImage(interaction.user.id, {
61
+ guildId: interaction.guild?.id,
62
+ useRoleColor: true,
63
+ presenceStatus: interaction.member?.presence?.status,
64
+ customBadges: extraBadges.length ? extraBadges : undefined,
65
+ badgesFrame: true,
66
+ });
67
+
68
+ await interaction.reply({ files: [toAttachment(buffer, "profile", "png")] });
69
+ }
70
+ });
55
71
 
56
- const buffer = await profileImage(userId, { guildId, useRoleColor: true, theme: "discord" });
57
- await interaction.reply({ files: [toAttachment(buffer, "profile", "png")] });
72
+ client.login(process.env.TOKEN);
58
73
  ```
59
74
 
60
75
  Every other card, the music adapters, theming, caching, error handling, and output options are documented with live examples at **[documentation](https://kira-arts.chocofactory.dev/)**.
61
76
 
77
+ ## ๐Ÿƒ Cards at a glance
78
+
79
+ | Card | Function | What it's for |
80
+ | --------------- | --------------------------------- | ------------------------------------------- |
81
+ | Profile | `profileImage()` | Avatar, badges, nameplate, server tag, rank |
82
+ | Welcome / Leave | `welcomeImage()` / `leaveImage()` | Member join/leave events |
83
+ | Level Up | `levelUpImage()` | XP progress bar on level-up |
84
+ | Achievement | `achievementImage()` | Unlockable achievements with rarity tiers |
85
+ | Leaderboard | `leaderboardImage()` | Server ranking table |
86
+ | Ship | `shipImage()` | Compatibility between two users |
87
+ | Now Playing | `nowPlayingImage()` | Music player card with source detection |
88
+
89
+ ## ๐Ÿค Contributors
90
+
91
+ Kira-Arts is developed and maintained by the WordDevs community.
92
+
93
+ <a href="https://github.com/worddevs/kira-arts/graphs/contributors">
94
+ <img src="https://contrib.rocks/image?repo=worddevs/kira-arts" alt="Contributors" />
95
+ </a>
96
+
62
97
  ## ๐Ÿ“„ License
63
98
 
64
- Apache-2.0 ยฉ [worddevs](https://github.com/worddevs)
99
+ Kira-Arts is released under the **Apache-2.0 License**.
100
+
101
+ Copyright ยฉ [worddevs](https://github.com/worddevs)
65
102
 
66
103
  ## ๐Ÿ”— Links
67
104
 
68
- - Documentation: https://kira-arts.chocofactory.dev/
69
- - Repository: https://github.com/worddevs/kira-arts
70
- - Issues: https://github.com/worddevs/kira-arts/issues
105
+ - ๐Ÿ“š **Documentation:** https://kira-arts.chocofactory.dev/
106
+ - ๐Ÿ“ฆ **NPM:** https://www.npmjs.com/package/kira-arts
107
+ - ๐Ÿ’ป **Repository:** https://github.com/worddevs/kira-arts
108
+ - ๐Ÿ› **Issues:** https://github.com/worddevs/kira-arts/issues
109
+ - ๐Ÿ”€ **Contributing:** https://github.com/worddevs/kira-arts/blob/main/CONTRIBUTING.md
110
+
111
+ ---
112
+
113
+ <p align="center">
114
+ Made with ๐Ÿ’ž by <a href="https://github.com/worddevs">WordDevs</a>
115
+ </p>
116
+
117
+ <p align="center">
118
+ <sub>Built with TypeScript and powered by <code>@napi-rs/canvas</code>.</sub>
119
+ </p>
package/dist/index.cjs CHANGED
@@ -441,7 +441,6 @@ function normalizeDisplayText(value) {
441
441
  if (typeof value !== "string") return value;
442
442
  try {
443
443
  const normalized = value.normalize("NFKC");
444
- if (normalized !== value) return normalized;
445
444
  return applyMathScriptFallback(normalized);
446
445
  } catch {
447
446
  return value;
@@ -647,7 +646,7 @@ function getDateOrString(dateInput, createdTimestamp, localDateType = "en") {
647
646
  return (/* @__PURE__ */ new Date(+createdTimestamp)).toLocaleDateString(localDateType, dateOptions);
648
647
  }
649
648
  }
650
- function formatGiveawayEndText(endsAt, from = Date.now(), localDateType = "es") {
649
+ function formatGiveawayEndText(endsAt, from = Date.now(), localDateType = "en") {
651
650
  const target = endsAt instanceof Date ? endsAt : new Date(endsAt);
652
651
  const diffMs = target.getTime() - from;
653
652
  const isEs = localDateType.startsWith("es");
@@ -718,6 +717,14 @@ function formatDuration(ms) {
718
717
  }
719
718
  return `${minutes}:${seconds}`;
720
719
  }
720
+ function normalizeDisplayText2(text) {
721
+ if (!text) return text;
722
+ try {
723
+ return text.normalize("NFKC");
724
+ } catch {
725
+ return text;
726
+ }
727
+ }
721
728
 
722
729
  // Public/ProfileImage.files.json
723
730
  var ProfileImage_files_default = {
@@ -905,7 +912,17 @@ async function genAvatarFrame(data, options) {
905
912
  const ctx = canvas.getContext("2d");
906
913
  const frameUrl = data?.decoration?.avatarFrame;
907
914
  const avatarFrame = await (0, import_canvas6.loadImage)(frameUrl);
908
- ctx.drawImage(avatarFrame, 25, 18, 269, 269);
915
+ if (options?.squareAvatar) {
916
+ const roundValue = 30 * (269 / 225);
917
+ ctx.save();
918
+ ctx.beginPath();
919
+ ctx.roundRect(25, 18, 269, 269, [roundValue]);
920
+ ctx.clip();
921
+ ctx.drawImage(avatarFrame, 25, 18, 269, 269);
922
+ ctx.restore();
923
+ } else {
924
+ ctx.drawImage(avatarFrame, 25, 18, 269, 269);
925
+ }
909
926
  if (options?.presenceStatus) {
910
927
  canvas = await cutAvatarStatus(canvas, options);
911
928
  }
@@ -1879,6 +1896,36 @@ function applyTextEffect(ctx, effect, glowColor) {
1879
1896
 
1880
1897
  // Src/Utils/MemberEeventCard/pills.ts
1881
1898
  var import_canvas14 = require("@napi-rs/canvas");
1899
+ var JOIN_DOT_COLOR = "#3ba55d";
1900
+ var LEAVE_DOT_COLOR = "#ed4245";
1901
+ function drawEventKindPill(ctx, width, kind, label) {
1902
+ const dotColor = kind === "welcome" ? JOIN_DOT_COLOR : LEAVE_DOT_COLOR;
1903
+ const dotSize = 7;
1904
+ const paddingX = 12;
1905
+ const pillHeight = 26;
1906
+ const gap = 8;
1907
+ ctx.font = withFallback("12px Helvetica Bold");
1908
+ const textWidth = ctx.measureText(label).width;
1909
+ const pillWidth = paddingX * 2 + dotSize + gap + textWidth;
1910
+ const pillX = width / 2 - pillWidth / 2;
1911
+ const pillY = 16;
1912
+ ctx.save();
1913
+ ctx.beginPath();
1914
+ ctx.roundRect(pillX, pillY, pillWidth, pillHeight, [pillHeight / 2]);
1915
+ ctx.fillStyle = "rgba(10, 10, 14, 0.55)";
1916
+ ctx.fill();
1917
+ ctx.restore();
1918
+ const dotCx = pillX + paddingX + dotSize / 2;
1919
+ const dotCy = pillY + pillHeight / 2;
1920
+ ctx.beginPath();
1921
+ ctx.arc(dotCx, dotCy, dotSize / 2, 0, Math.PI * 2);
1922
+ ctx.fillStyle = dotColor;
1923
+ ctx.fill();
1924
+ ctx.textAlign = "left";
1925
+ ctx.fillStyle = "#ffffff";
1926
+ ctx.font = withFallback("12px Helvetica Bold");
1927
+ ctx.fillText(label, pillX + paddingX + dotSize + gap, pillY + pillHeight / 2 + 4);
1928
+ }
1882
1929
  async function drawServerTagPill(ctx, serverTag) {
1883
1930
  const text = truncateText(serverTag.text, 12);
1884
1931
  const iconSize = serverTag.badgeURL ? 18 : 0;
@@ -1952,19 +1999,35 @@ async function drawMemberEventCard(ctx, width, height, avatarUrl, username, layo
1952
1999
  if (bannerImage) {
1953
2000
  drawCoverImage(ctx, bannerImage, 0, 0, width, height);
1954
2001
  const overlay = ctx.createLinearGradient(0, 0, 0, height);
1955
- overlay.addColorStop(0, "rgba(8, 9, 12, 0.1)");
1956
- overlay.addColorStop(0.3, "rgba(8, 9, 12, 0.3)");
1957
- overlay.addColorStop(0.55, "rgba(13, 15, 20, 0.55)");
1958
- overlay.addColorStop(1, "rgba(13, 15, 20, 0.9)");
2002
+ overlay.addColorStop(0, "rgba(8, 9, 12, 0.15)");
2003
+ overlay.addColorStop(0.3, "rgba(8, 9, 12, 0.35)");
2004
+ overlay.addColorStop(0.55, "rgba(13, 15, 20, 0.6)");
2005
+ overlay.addColorStop(1, "rgba(13, 15, 20, 0.92)");
1959
2006
  ctx.fillStyle = overlay;
1960
2007
  ctx.fillRect(0, 0, width, height);
1961
2008
  } else {
1962
- const grd = ctx.createLinearGradient(0, 0, width, BANNER_HEIGHT);
1963
- grd.addColorStop(0, hexToRgba(layout.accentColor, 0.85));
1964
- grd.addColorStop(1, hexToRgba(layout.accentColor, 0.3));
2009
+ const grd = ctx.createLinearGradient(0, 0, width, BANNER_HEIGHT * 1.4);
2010
+ grd.addColorStop(0, hexToRgba(layout.accentColor, 0.95));
2011
+ grd.addColorStop(0.55, hexToRgba(layout.accentColor, 0.55));
2012
+ grd.addColorStop(1, hexToRgba(layout.accentColor, 0.18));
1965
2013
  ctx.fillStyle = grd;
1966
2014
  ctx.fillRect(0, 0, width, BANNER_HEIGHT);
1967
2015
  }
2016
+ const spotlight = ctx.createRadialGradient(
2017
+ width / 2,
2018
+ BANNER_HEIGHT,
2019
+ 0,
2020
+ width / 2,
2021
+ BANNER_HEIGHT,
2022
+ BANNER_HEIGHT * 1.15
2023
+ );
2024
+ spotlight.addColorStop(0, hexToRgba(layout.accentColor, 0.35));
2025
+ spotlight.addColorStop(1, hexToRgba(layout.accentColor, 0));
2026
+ ctx.fillStyle = spotlight;
2027
+ ctx.fillRect(0, 0, width, height);
2028
+ if (layout.kindLabel) {
2029
+ drawEventKindPill(ctx, width, layout.kind, layout.kindLabel);
2030
+ }
1968
2031
  if (layout.serverTag) {
1969
2032
  await drawServerTagPill(ctx, layout.serverTag);
1970
2033
  }
@@ -2020,6 +2083,13 @@ async function drawMemberEventCard(ctx, width, height, avatarUrl, username, layo
2020
2083
  ctx.font = withFallback(`${Math.round(30 * fontScale)}px Helvetica Bold`);
2021
2084
  ctx.fillStyle = layout.usernameColor ? parseHex(layout.usernameColor) : "#FFFFFF";
2022
2085
  ctx.fillText(truncateText(username, 24), width / 2, cursorY);
2086
+ applyTextEffect(ctx, "none", glowColor);
2087
+ const dividerY = cursorY + 14 * fontScale;
2088
+ const dividerWidth = 36 * fontScale;
2089
+ ctx.beginPath();
2090
+ ctx.roundRect(width / 2 - dividerWidth / 2, dividerY, dividerWidth, 3, [2]);
2091
+ ctx.fillStyle = hexToRgba(layout.accentColor, 0.9);
2092
+ ctx.fill();
2023
2093
  if (layout.message) {
2024
2094
  cursorY += 32 * fontScale;
2025
2095
  applyTextEffect(ctx, layout.messageEffect, glowColor);
@@ -2028,11 +2098,11 @@ async function drawMemberEventCard(ctx, width, height, avatarUrl, username, layo
2028
2098
  ctx.fillText(truncateText(layout.message, 64), width / 2, cursorY);
2029
2099
  }
2030
2100
  applyTextEffect(ctx, "none", glowColor);
2031
- if (layout.memberCount != null) {
2101
+ if (layout.memberCount != null && layout.memberCountText) {
2032
2102
  cursorY += 30 * fontScale;
2033
2103
  ctx.font = withFallback(`bold ${Math.round(16 * fontScale)}px Helvetica Bold`);
2034
2104
  ctx.fillStyle = hexToRgba(layout.accentColor, 1);
2035
- ctx.fillText(`Miembro #${layout.memberCount}`, width / 2, cursorY);
2105
+ ctx.fillText(layout.memberCountText, width / 2, cursorY);
2036
2106
  }
2037
2107
  if (layout.dateText) {
2038
2108
  cursorY += 26 * fontScale;
@@ -2059,7 +2129,16 @@ var DEFAULT_ACCENT2 = "#2DD4BF";
2059
2129
  function formatMessage(template, username, guildName, memberCount) {
2060
2130
  return template.replace(/\{username\}/g, username).replace(/\{user\}/g, username).replace(/\{server\}/g, guildName).replace(/\{memberCount\}/g, memberCount != null ? String(memberCount) : "");
2061
2131
  }
2062
- async function renderMemberEventCard(userId, guildName, defaultMessage, options = {}) {
2132
+ function formatMemberCount(memberCount, localDateType) {
2133
+ const isEs = localDateType.startsWith("es");
2134
+ return isEs ? `Member #${memberCount}` : `Member #${memberCount}`;
2135
+ }
2136
+ function formatKindLabel(kind, localDateType) {
2137
+ const isEs = localDateType.startsWith("es");
2138
+ if (kind === "welcome") return isEs ? "NEW MEMBER" : "NEW MEMBER";
2139
+ return isEs ? "MEMBER LEFT" : "MEMBER LEFT";
2140
+ }
2141
+ async function renderMemberEventCard(userId, guildName, defaultMessage, options = {}, kind = "welcome") {
2063
2142
  if (!userId || typeof userId !== "string") {
2064
2143
  throw new KiraError("A valid userId is required", "VALIDATION" /* Validation */);
2065
2144
  }
@@ -2067,7 +2146,7 @@ async function renderMemberEventCard(userId, guildName, defaultMessage, options
2067
2146
  throw new KiraError("A valid guildName is required", "VALIDATION" /* Validation */);
2068
2147
  }
2069
2148
  const data = await fetchUserData(userId, options.guildId, options.bypassCache);
2070
- const username = data.basicInfo.globalName || data.basicInfo.username;
2149
+ const username = normalizeDisplayText2(data.basicInfo.globalName || data.basicInfo.username);
2071
2150
  const avatarUrl = data.assets.avatarURL ?? data.assets.defaultAvatarURL;
2072
2151
  const palette = getThemePalette(options.theme);
2073
2152
  const accentColor = options.accentColor ? parseHex(options.accentColor) : palette?.accentColor ? parseHex(palette.accentColor) : DEFAULT_ACCENT2;
@@ -2080,16 +2159,23 @@ async function renderMemberEventCard(userId, guildName, defaultMessage, options
2080
2159
  roleColor: data.decoration.roleColor,
2081
2160
  fallback: palette?.borderColor ?? [accentColor]
2082
2161
  });
2162
+ const localDateType = options.localDateType ?? "es";
2083
2163
  const rawMessage = options.message ?? defaultMessage;
2084
- const message = formatMessage(rawMessage, username, guildName, options.memberCount);
2085
- const dateText = options.showDate ? getDateOrString(options.date, Date.now(), options.localDateType ?? "es") : void 0;
2164
+ const message = normalizeDisplayText2(
2165
+ formatMessage(rawMessage, username, normalizeDisplayText2(guildName), options.memberCount)
2166
+ );
2167
+ const dateText = options.showDate ? getDateOrString(options.date, Date.now(), localDateType) : void 0;
2168
+ const memberCountText = options.memberCount != null ? formatMemberCount(options.memberCount, localDateType) : void 0;
2086
2169
  const fontScale = options.fontScale && options.fontScale > 0 ? options.fontScale : 1;
2087
2170
  const avatarFrameUrl = options.showAvatarDecoration ? data.decoration.avatarFrame : null;
2088
2171
  const serverTag = options.showServerTag ? data.decoration.serverTag : null;
2089
2172
  const badges = options.showBadges ? data.assets.badges : void 0;
2090
2173
  const layout = {
2174
+ kind,
2175
+ kindLabel: formatKindLabel(kind, localDateType),
2091
2176
  message,
2092
2177
  memberCount: options.memberCount,
2178
+ memberCountText,
2093
2179
  dateText,
2094
2180
  accentColor,
2095
2181
  borderColors,
@@ -2116,15 +2202,15 @@ async function renderMemberEventCard(userId, guildName, defaultMessage, options
2116
2202
  }
2117
2203
 
2118
2204
  // Src/Cards/welcomeImage.ts
2119
- var DEFAULT_WELCOME_MESSAGE = "\xA1Welcome to {server}!";
2205
+ var DEFAULT_WELCOME_MESSAGE = "Welcome to {server}!";
2120
2206
  async function welcomeImage(userId, guildName, options = {}) {
2121
- return renderMemberEventCard(userId, guildName, DEFAULT_WELCOME_MESSAGE, options);
2207
+ return renderMemberEventCard(userId, guildName, DEFAULT_WELCOME_MESSAGE, options, "welcome");
2122
2208
  }
2123
2209
 
2124
2210
  // Src/Cards/leaveImage.ts
2125
- var DEFAULT_LEAVE_MESSAGE = "{username} has left {server}.";
2211
+ var DEFAULT_LEAVE_MESSAGE = "{username} has left {server}";
2126
2212
  async function leaveImage(userId, guildName, options = {}) {
2127
- return renderMemberEventCard(userId, guildName, DEFAULT_LEAVE_MESSAGE, options);
2213
+ return renderMemberEventCard(userId, guildName, DEFAULT_LEAVE_MESSAGE, options, "leave");
2128
2214
  }
2129
2215
 
2130
2216
  // Src/Utils/LevelUpCard/constants.ts
@@ -2541,7 +2627,7 @@ var RARITY_COLORS = {
2541
2627
  };
2542
2628
  var RARITY_LABELS = {
2543
2629
  common: "Common",
2544
- rare: "Queer",
2630
+ rare: "Rare",
2545
2631
  epic: "Epic",
2546
2632
  legendary: "Legendary"
2547
2633
  };
@@ -3730,7 +3816,7 @@ function drawConfetti(ctx, width, height, colors) {
3730
3816
  ctx.restore();
3731
3817
  }
3732
3818
  function drawStatusPill(ctx, x, y, active, color) {
3733
- const text = active ? "ACTIVO" : "FINALIZADO";
3819
+ const text = active ? "ACTIVE" : "ENDED";
3734
3820
  ctx.save();
3735
3821
  ctx.font = withFallback("bold 12px Helvetica Bold");
3736
3822
  const textWidth = ctx.measureText(text).width;
@@ -3825,7 +3911,7 @@ async function drawGiveawayCard(ctx, width, height, layout) {
3825
3911
  ctx.textAlign = "left";
3826
3912
  ctx.textBaseline = "middle";
3827
3913
  ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
3828
- ctx.fillText("SORTEO", PADDING2 + 32, cursorY + 12);
3914
+ ctx.fillText("GIVEAWAY", PADDING2 + 32, cursorY + 12);
3829
3915
  drawStatusPill(ctx, width - PADDING2, cursorY, layout.status !== "ended", statusColor);
3830
3916
  cursorY += 56;
3831
3917
  ctx.textAlign = "left";
@@ -3861,13 +3947,13 @@ async function drawGiveawayCard(ctx, width, height, layout) {
3861
3947
  const midGap = 26;
3862
3948
  const trophyGap = 12;
3863
3949
  const maxRowWidth = width - PADDING2 * 2;
3864
- const winnersLabel = `${layout.winnersCount} ${layout.winnersCount === 1 ? "ganador" : "ganadores"}`;
3950
+ const winnersLabel = `${layout.winnersCount} ${layout.winnersCount === 1 ? "winner" : "winners"}`;
3865
3951
  ctx.font = withFallback("15px Helvetica");
3866
3952
  const winnersLabelWidth = ctx.measureText(winnersLabel).width;
3867
3953
  const fixedWidth = HOST_AVATAR_SIZE + avatarGap + midGap + trophySize + trophyGap + winnersLabelWidth;
3868
3954
  const hostMaxWidth = layout.hostName ? Math.max(60, maxRowWidth - fixedWidth) : 0;
3869
3955
  ctx.font = withFallback("15px Helvetica");
3870
- const hostText = layout.hostName ? truncateTextToWidth(ctx, `Organiza ${layout.hostName}`, hostMaxWidth) : "";
3956
+ const hostText = layout.hostName ? truncateTextToWidth(ctx, `Hosted by ${layout.hostName}`, hostMaxWidth) : "";
3871
3957
  const hostTextWidth = layout.hostName ? ctx.measureText(hostText).width : 0;
3872
3958
  const rowWidth = layout.hostName ? HOST_AVATAR_SIZE + avatarGap + hostTextWidth + midGap + trophySize + trophyGap + winnersLabelWidth : trophySize + trophyGap + winnersLabelWidth;
3873
3959
  let rowX = width / 2 - rowWidth / 2;
@@ -3901,7 +3987,7 @@ async function drawGiveawayCard(ctx, width, height, layout) {
3901
3987
  const infoColor = "rgba(255, 255, 255, 0.5)";
3902
3988
  ctx.font = withFallback("13px Helvetica");
3903
3989
  const dateTextWidth = ctx.measureText(layout.dateText).width;
3904
- const participantsText = layout.participantsCount !== void 0 ? `${layout.participantsCount} ${layout.participantsCount === 1 ? "participante" : "participantes"}` : void 0;
3990
+ const participantsText = layout.participantsCount !== void 0 ? `${layout.participantsCount} ${layout.participantsCount === 1 ? "participant" : "participants"}` : void 0;
3905
3991
  const separator = " \u2022 ";
3906
3992
  const separatorWidth = participantsText ? ctx.measureText(separator).width : 0;
3907
3993
  const participantsTextWidth = participantsText ? ctx.measureText(participantsText).width : 0;
@@ -3922,7 +4008,7 @@ async function drawGiveawayCard(ctx, width, height, layout) {
3922
4008
  }
3923
4009
  if (hasWinners) {
3924
4010
  const shown = winners.slice(0, MAX_WINNERS_SHOWN);
3925
- const remaining = winners.length - shown.length;
4011
+ const remaining = layout.winnersCount - shown.length;
3926
4012
  const slotWidth = 92;
3927
4013
  const rowWidth2 = shown.length * slotWidth;
3928
4014
  const startX = width / 2 - rowWidth2 / 2 + slotWidth / 2;
@@ -3938,7 +4024,7 @@ async function drawGiveawayCard(ctx, width, height, layout) {
3938
4024
  if (remaining > 0) {
3939
4025
  ctx.font = withFallback("13px Helvetica");
3940
4026
  ctx.fillStyle = "rgba(255, 255, 255, 0.55)";
3941
- ctx.fillText(`+${remaining} m\xE1s`, width / 2, height - PADDING2 + 4);
4027
+ ctx.fillText(`+${remaining} more`, width / 2, height - PADDING2 + 4);
3942
4028
  }
3943
4029
  }
3944
4030
  ctx.restore();
@@ -3965,6 +4051,7 @@ async function giveawayImage(prize, options = {}) {
3965
4051
  const hostData = options.hostId ? await fetchUserData(options.hostId, options.guildId, bypassCache) : void 0;
3966
4052
  const hostName = options.hostName ?? hostData?.basicInfo.globalName ?? hostData?.basicInfo.username;
3967
4053
  const hostAvatarUrl = options.hostAvatar ?? hostData?.assets.avatarURL ?? hostData?.assets.defaultAvatarURL;
4054
+ const totalWinnerIds = options.winnerIds?.length ?? 0;
3968
4055
  const winnerIds = options.winnerIds?.slice(0, MAX_WINNERS_SHOWN) ?? [];
3969
4056
  const winnerDataList = await Promise.all(
3970
4057
  winnerIds.map((winnerId) => fetchUserData(winnerId, options.guildId, bypassCache))
@@ -3973,7 +4060,7 @@ async function giveawayImage(prize, options = {}) {
3973
4060
  username: data.basicInfo.globalName || data.basicInfo.username,
3974
4061
  avatarUrl: data.assets.avatarURL ?? data.assets.defaultAvatarURL
3975
4062
  }));
3976
- const winnersCount = options.winnersCount ?? (winnerIds.length > 0 ? winnerIds.length : 1);
4063
+ const winnersCount = options.winnersCount ?? (totalWinnerIds > 0 ? totalWinnerIds : 1);
3977
4064
  const palette = getThemePalette(options.theme);
3978
4065
  const accentColor = options.accentColor ? parseHex(options.accentColor) : palette?.accentColor ?? DEFAULT_ACCENT6;
3979
4066
  const borderColors = resolveCardColors({
@@ -3985,7 +4072,7 @@ async function giveawayImage(prize, options = {}) {
3985
4072
  roleColor: hostData?.decoration.roleColor,
3986
4073
  fallback: palette?.borderColor ?? [accentColor]
3987
4074
  });
3988
- const dateText = status === "ended" ? options.endedText ?? "Sorteo finalizado" : options.endsAt ? formatGiveawayEndText(options.endsAt, Date.now(), options.localDateType ?? "es") : void 0;
4075
+ const dateText = status === "ended" ? options.endedText ?? "Giveaway ended" : options.endsAt ? formatGiveawayEndText(options.endsAt, Date.now(), options.localDateType ?? "en") : void 0;
3989
4076
  const layout = {
3990
4077
  prize,
3991
4078
  description: options.description,