@spatulox/simplediscordbot 3.0.3 → 3.1.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/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
1
1
  # Changelog
2
2
  Date format : dd/mm/yyyy
3
3
 
4
+ ### 21/09/2026 - 3.1.1
5
+ - Add :
6
+ - `ComponentManager.chart(container, chart, separator?)` : adds one chart or an array of charts to a container, like `field()` / `mediaGallery()` / `selectMenu()` do, instead of calling `container.addTextDisplayComponents()` by hand. No separator by default
7
+
8
+ ### 21/09/2026 - 3.1.0
9
+ - Add :
10
+ - `ChartManager` : text based charts for Components V2, since Discord has no chart component. `progressBar()` / `progressBars()` draw unicode gauges (`CPU : ███░░░░░░░ 32.7 %`), `sparkline()` / `sparklines()` draw one line curves (`▁▂▃▅▇█▆▄▃▂▁`). Each method returns a `TextDisplayBuilder` to drop into a `ContainerBuilder`, and the grouped forms render every row in a single one so a dashboard costs 1 component instead of N against the 40 components budget of a message
11
+ - `sparklines()` takes an opt-in `sharedScale` so stacked curves are scaled against the same bounds and stay comparable, instead of each one filling the whole height
12
+ - Values are clamped (`value > max`, negative values, `max = 0`), a constant serie renders as a straight line instead of dividing by zero, an empty serie or an empty row list renders `—` (`TextDisplayBuilder.setContent()` rejects an empty string), and non finite points are dropped instead of flattening the whole curve
13
+
4
14
  ### 28/04/2026 - 2.2.1
5
15
  - Changes :
6
16
  - Add dependency to @spatulox/utils
package/README.md CHANGED
@@ -13,6 +13,7 @@
13
13
  > - Simple Log package
14
14
  > - Provides easy Managers to avoid repetitive code everywhere
15
15
  > - Simple yet powerful builders (Embeds, Modals, SelectMenus, Components, Buttons) that rely on discord.js for full compatibility
16
+ > - Text based charts (progress bars, sparklines) for Components V2, since Discord has no chart component
16
17
 
17
18
  # Don't forget to check the [wiki](https://github.com/spatulox-discord/SimpleDiscordBot/wiki)
18
19
 
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { EmbedBuilder, ContainerBuilder, ActionRowBuilder, MessageActionRowComponentBuilder, Message, TextChannel, DMChannel, ThreadChannel, MessageCreateOptions, User, GuildMember, BaseInteraction, InteractionResponse, Client, ActivityType, InteractionDeferReplyOptions, InteractionReplyOptions, InteractionEditReplyOptions, Snowflake, WebhookMessageCreateOptions, EmojiResolvable, Guild, BanOptions, GuildBasedChannel, GuildChannelCreateOptions, ForumChannel, NewsChannel, StageChannel, StartThreadOptions, VoiceChannel, Invite, Channel, Collection, GuildBan, ModalBuilder, StringSelectMenuBuilder, UserSelectMenuBuilder, RoleSelectMenuBuilder, MentionableSelectMenuBuilder, ChannelSelectMenuBuilder, ChannelType, SeparatorSpacingSize, ButtonBuilder, AttachmentBuilder, ButtonStyle } from 'discord.js';
1
+ import { EmbedBuilder, ContainerBuilder, ActionRowBuilder, MessageActionRowComponentBuilder, Message, TextChannel, DMChannel, ThreadChannel, MessageCreateOptions, User, GuildMember, BaseInteraction, InteractionResponse, Client, ActivityType, InteractionDeferReplyOptions, InteractionReplyOptions, InteractionEditReplyOptions, Snowflake, WebhookMessageCreateOptions, EmojiResolvable, Guild, BanOptions, GuildBasedChannel, GuildChannelCreateOptions, ForumChannel, NewsChannel, StageChannel, StartThreadOptions, VoiceChannel, Invite, Channel, Collection, GuildBan, ModalBuilder, StringSelectMenuBuilder, UserSelectMenuBuilder, RoleSelectMenuBuilder, MentionableSelectMenuBuilder, ChannelSelectMenuBuilder, ChannelType, SeparatorSpacingSize, ButtonBuilder, TextDisplayBuilder, AttachmentBuilder, ButtonStyle } from 'discord.js';
2
2
  import { BaseSelectMenuBuilder } from '@discordjs/builders';
3
3
  export { CacheManager, FileManager, Log, SimpleMutex, Time } from '@spatulox/utils';
4
4
 
@@ -701,6 +701,14 @@ declare class ComponentManager {
701
701
  * Multiple fields
702
702
  */
703
703
  static fields(container: ContainerBuilder, fields: ComponentManagerField[]): ContainerBuilder;
704
+ /**
705
+ * Add chart(s) built by the ChartManager, so a dashboard reads like the other helpers :
706
+ * `ComponentManager.chart(container, ChartManager.progressBars([...]))` instead of
707
+ * `container.addTextDisplayComponents(...)`.
708
+ * No separator by default : stacked charts are meant to be read as one block
709
+ */
710
+ static chart(container: ContainerBuilder, chart: TextDisplayBuilder[], separator?: SeparatorSpacingSize | false): ContainerBuilder;
711
+ static chart(container: ContainerBuilder, chart: TextDisplayBuilder, separator?: SeparatorSpacingSize | false): ContainerBuilder;
704
712
  /**
705
713
  * Add a media gallery (links)
706
714
  */
@@ -737,6 +745,100 @@ declare class ComponentManager {
737
745
  static toInteractionEdit(container: ContainerBuilder, file?: AttachmentBuilder | AttachmentBuilder[] | null, footer?: boolean): InteractionEditReplyOptions;
738
746
  }
739
747
 
748
+ interface ProgressBarOptions {
749
+ /** Number of characters of the bar itself, default 10 */
750
+ width?: number;
751
+ /** Character of the filled part, default "█" */
752
+ filled?: string;
753
+ /** Character of the empty part, default "░" */
754
+ empty?: string;
755
+ /** Append the numeric value after the bar, default true */
756
+ showValue?: boolean;
757
+ /** Unit printed after the value, default "%" */
758
+ unit?: string;
759
+ /** Decimals of the printed value, default 1 */
760
+ decimals?: number;
761
+ /**
762
+ * Wrap the whole render in a ``` block. Discord renders TextDisplay with a proportional
763
+ * font, so labels of different widths never line up : only a code block gives a real
764
+ * monospace alignment. Default false.
765
+ */
766
+ codeBlock?: boolean;
767
+ }
768
+ interface SparklineOptions {
769
+ /** Fixed low bound of the scale, default min(values) */
770
+ min?: number;
771
+ /** Fixed high bound of the scale, default max(values) */
772
+ max?: number;
773
+ /** Only keep the N most recent points, default 50 */
774
+ maxPoints?: number;
775
+ /** Append the last value after the curve, default true */
776
+ showValue?: boolean;
777
+ /** Unit printed after the value, default "" */
778
+ unit?: string;
779
+ /** Decimals of the printed value, default 1 */
780
+ decimals?: number;
781
+ /** See ProgressBarOptions.codeBlock. Default false */
782
+ codeBlock?: boolean;
783
+ }
784
+ interface ProgressBarRow {
785
+ label: string;
786
+ value: number;
787
+ /** Default 100 */
788
+ max?: number;
789
+ /** Overrides the options given to progressBars() for this row only */
790
+ options?: ProgressBarOptions;
791
+ }
792
+ interface SparklineRow {
793
+ label: string;
794
+ values: number[];
795
+ /** Overrides the options given to sparklines() for this row only */
796
+ options?: SparklineOptions;
797
+ }
798
+ interface SparklinesOptions extends SparklineOptions {
799
+ /**
800
+ * Scale every curve against the same bounds, deduced from all the series at once.
801
+ * Off by default : each curve then uses its own min/max and fills the whole height,
802
+ * which reads better alone but makes two stacked curves impossible to compare.
803
+ */
804
+ sharedScale?: boolean;
805
+ }
806
+ declare class ChartManager {
807
+ /**
808
+ * A single gauge : `CPU : ███░░░░░░░ 32.7 %`
809
+ */
810
+ static progressBar(label: string, value: number, max?: number, options?: ProgressBarOptions): TextDisplayBuilder;
811
+ /**
812
+ * Several gauges in ONE TextDisplay, so a dashboard of 6 metrics still costs 1 component.
813
+ * Labels are padded to the longest one so the bars start at the same column.
814
+ */
815
+ static progressBars(rows: ProgressBarRow[], options?: ProgressBarOptions): TextDisplayBuilder;
816
+ /**
817
+ * A one line curve : `RAM : ▁▂▃▅▇█▆▄▃▂▁ 62 %`. One block character per point, the last
818
+ * value is printed at the end because a sparkline alone carries no scale.
819
+ */
820
+ static sparkline(label: string, values: number[], options?: SparklineOptions): TextDisplayBuilder;
821
+ /**
822
+ * Several curves in ONE TextDisplay, the sparkline counterpart of progressBars().
823
+ * Labels are padded to the longest one so every curve starts at the same column.
824
+ */
825
+ static sparklines(rows: SparklineRow[], options?: SparklinesOptions): TextDisplayBuilder;
826
+ /**
827
+ * Only fills the bounds that were not given explicitly, and only when asked : a row
828
+ * setting its own min/max still wins, since row options are spread after these.
829
+ */
830
+ private static withSharedScale;
831
+ private static renderBar;
832
+ private static renderSpark;
833
+ private static formatValue;
834
+ /**
835
+ * TextDisplayBuilder.setContent() rejects an empty string, so an empty row list must
836
+ * still render something rather than throw at build time.
837
+ */
838
+ private static wrap;
839
+ private static clamp01;
840
+ }
841
+
740
842
  interface ButtonOptions {
741
843
  label?: string;
742
844
  emoji?: string;
@@ -839,4 +941,4 @@ declare const SimpleDiscordBotInfo: {
839
941
  license: string;
840
942
  };
841
943
 
842
- export { Bot, type BotConfig, BotEnv, ButtonManager, type ButtonOptions, ComponentManager, type ComponentManagerCreate, type ComponentManagerField, type ComponentManagerFileInput, DiscordRegex, EmbedManager, GuildManager, type ModalField, ModalFieldType, ModalManager, type RandomBotActivity, ReactionManager, type SelectMenuCreateOption, type SelectMenuList, SelectMenuManager, SimpleColor, SimpleDiscordBotInfo, UserManager, WebhookManager };
944
+ export { Bot, type BotConfig, BotEnv, ButtonManager, type ButtonOptions, ChartManager, ComponentManager, type ComponentManagerCreate, type ComponentManagerField, type ComponentManagerFileInput, DiscordRegex, EmbedManager, GuildManager, type ModalField, ModalFieldType, ModalManager, type ProgressBarOptions, type ProgressBarRow, type RandomBotActivity, ReactionManager, type SelectMenuCreateOption, type SelectMenuList, SelectMenuManager, SimpleColor, SimpleDiscordBotInfo, type SparklineOptions, type SparklineRow, type SparklinesOptions, UserManager, WebhookManager };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { EmbedBuilder, ContainerBuilder, ActionRowBuilder, MessageActionRowComponentBuilder, Message, TextChannel, DMChannel, ThreadChannel, MessageCreateOptions, User, GuildMember, BaseInteraction, InteractionResponse, Client, ActivityType, InteractionDeferReplyOptions, InteractionReplyOptions, InteractionEditReplyOptions, Snowflake, WebhookMessageCreateOptions, EmojiResolvable, Guild, BanOptions, GuildBasedChannel, GuildChannelCreateOptions, ForumChannel, NewsChannel, StageChannel, StartThreadOptions, VoiceChannel, Invite, Channel, Collection, GuildBan, ModalBuilder, StringSelectMenuBuilder, UserSelectMenuBuilder, RoleSelectMenuBuilder, MentionableSelectMenuBuilder, ChannelSelectMenuBuilder, ChannelType, SeparatorSpacingSize, ButtonBuilder, AttachmentBuilder, ButtonStyle } from 'discord.js';
1
+ import { EmbedBuilder, ContainerBuilder, ActionRowBuilder, MessageActionRowComponentBuilder, Message, TextChannel, DMChannel, ThreadChannel, MessageCreateOptions, User, GuildMember, BaseInteraction, InteractionResponse, Client, ActivityType, InteractionDeferReplyOptions, InteractionReplyOptions, InteractionEditReplyOptions, Snowflake, WebhookMessageCreateOptions, EmojiResolvable, Guild, BanOptions, GuildBasedChannel, GuildChannelCreateOptions, ForumChannel, NewsChannel, StageChannel, StartThreadOptions, VoiceChannel, Invite, Channel, Collection, GuildBan, ModalBuilder, StringSelectMenuBuilder, UserSelectMenuBuilder, RoleSelectMenuBuilder, MentionableSelectMenuBuilder, ChannelSelectMenuBuilder, ChannelType, SeparatorSpacingSize, ButtonBuilder, TextDisplayBuilder, AttachmentBuilder, ButtonStyle } from 'discord.js';
2
2
  import { BaseSelectMenuBuilder } from '@discordjs/builders';
3
3
  export { CacheManager, FileManager, Log, SimpleMutex, Time } from '@spatulox/utils';
4
4
 
@@ -701,6 +701,14 @@ declare class ComponentManager {
701
701
  * Multiple fields
702
702
  */
703
703
  static fields(container: ContainerBuilder, fields: ComponentManagerField[]): ContainerBuilder;
704
+ /**
705
+ * Add chart(s) built by the ChartManager, so a dashboard reads like the other helpers :
706
+ * `ComponentManager.chart(container, ChartManager.progressBars([...]))` instead of
707
+ * `container.addTextDisplayComponents(...)`.
708
+ * No separator by default : stacked charts are meant to be read as one block
709
+ */
710
+ static chart(container: ContainerBuilder, chart: TextDisplayBuilder[], separator?: SeparatorSpacingSize | false): ContainerBuilder;
711
+ static chart(container: ContainerBuilder, chart: TextDisplayBuilder, separator?: SeparatorSpacingSize | false): ContainerBuilder;
704
712
  /**
705
713
  * Add a media gallery (links)
706
714
  */
@@ -737,6 +745,100 @@ declare class ComponentManager {
737
745
  static toInteractionEdit(container: ContainerBuilder, file?: AttachmentBuilder | AttachmentBuilder[] | null, footer?: boolean): InteractionEditReplyOptions;
738
746
  }
739
747
 
748
+ interface ProgressBarOptions {
749
+ /** Number of characters of the bar itself, default 10 */
750
+ width?: number;
751
+ /** Character of the filled part, default "█" */
752
+ filled?: string;
753
+ /** Character of the empty part, default "░" */
754
+ empty?: string;
755
+ /** Append the numeric value after the bar, default true */
756
+ showValue?: boolean;
757
+ /** Unit printed after the value, default "%" */
758
+ unit?: string;
759
+ /** Decimals of the printed value, default 1 */
760
+ decimals?: number;
761
+ /**
762
+ * Wrap the whole render in a ``` block. Discord renders TextDisplay with a proportional
763
+ * font, so labels of different widths never line up : only a code block gives a real
764
+ * monospace alignment. Default false.
765
+ */
766
+ codeBlock?: boolean;
767
+ }
768
+ interface SparklineOptions {
769
+ /** Fixed low bound of the scale, default min(values) */
770
+ min?: number;
771
+ /** Fixed high bound of the scale, default max(values) */
772
+ max?: number;
773
+ /** Only keep the N most recent points, default 50 */
774
+ maxPoints?: number;
775
+ /** Append the last value after the curve, default true */
776
+ showValue?: boolean;
777
+ /** Unit printed after the value, default "" */
778
+ unit?: string;
779
+ /** Decimals of the printed value, default 1 */
780
+ decimals?: number;
781
+ /** See ProgressBarOptions.codeBlock. Default false */
782
+ codeBlock?: boolean;
783
+ }
784
+ interface ProgressBarRow {
785
+ label: string;
786
+ value: number;
787
+ /** Default 100 */
788
+ max?: number;
789
+ /** Overrides the options given to progressBars() for this row only */
790
+ options?: ProgressBarOptions;
791
+ }
792
+ interface SparklineRow {
793
+ label: string;
794
+ values: number[];
795
+ /** Overrides the options given to sparklines() for this row only */
796
+ options?: SparklineOptions;
797
+ }
798
+ interface SparklinesOptions extends SparklineOptions {
799
+ /**
800
+ * Scale every curve against the same bounds, deduced from all the series at once.
801
+ * Off by default : each curve then uses its own min/max and fills the whole height,
802
+ * which reads better alone but makes two stacked curves impossible to compare.
803
+ */
804
+ sharedScale?: boolean;
805
+ }
806
+ declare class ChartManager {
807
+ /**
808
+ * A single gauge : `CPU : ███░░░░░░░ 32.7 %`
809
+ */
810
+ static progressBar(label: string, value: number, max?: number, options?: ProgressBarOptions): TextDisplayBuilder;
811
+ /**
812
+ * Several gauges in ONE TextDisplay, so a dashboard of 6 metrics still costs 1 component.
813
+ * Labels are padded to the longest one so the bars start at the same column.
814
+ */
815
+ static progressBars(rows: ProgressBarRow[], options?: ProgressBarOptions): TextDisplayBuilder;
816
+ /**
817
+ * A one line curve : `RAM : ▁▂▃▅▇█▆▄▃▂▁ 62 %`. One block character per point, the last
818
+ * value is printed at the end because a sparkline alone carries no scale.
819
+ */
820
+ static sparkline(label: string, values: number[], options?: SparklineOptions): TextDisplayBuilder;
821
+ /**
822
+ * Several curves in ONE TextDisplay, the sparkline counterpart of progressBars().
823
+ * Labels are padded to the longest one so every curve starts at the same column.
824
+ */
825
+ static sparklines(rows: SparklineRow[], options?: SparklinesOptions): TextDisplayBuilder;
826
+ /**
827
+ * Only fills the bounds that were not given explicitly, and only when asked : a row
828
+ * setting its own min/max still wins, since row options are spread after these.
829
+ */
830
+ private static withSharedScale;
831
+ private static renderBar;
832
+ private static renderSpark;
833
+ private static formatValue;
834
+ /**
835
+ * TextDisplayBuilder.setContent() rejects an empty string, so an empty row list must
836
+ * still render something rather than throw at build time.
837
+ */
838
+ private static wrap;
839
+ private static clamp01;
840
+ }
841
+
740
842
  interface ButtonOptions {
741
843
  label?: string;
742
844
  emoji?: string;
@@ -839,4 +941,4 @@ declare const SimpleDiscordBotInfo: {
839
941
  license: string;
840
942
  };
841
943
 
842
- export { Bot, type BotConfig, BotEnv, ButtonManager, type ButtonOptions, ComponentManager, type ComponentManagerCreate, type ComponentManagerField, type ComponentManagerFileInput, DiscordRegex, EmbedManager, GuildManager, type ModalField, ModalFieldType, ModalManager, type RandomBotActivity, ReactionManager, type SelectMenuCreateOption, type SelectMenuList, SelectMenuManager, SimpleColor, SimpleDiscordBotInfo, UserManager, WebhookManager };
944
+ export { Bot, type BotConfig, BotEnv, ButtonManager, type ButtonOptions, ChartManager, ComponentManager, type ComponentManagerCreate, type ComponentManagerField, type ComponentManagerFileInput, DiscordRegex, EmbedManager, GuildManager, type ModalField, ModalFieldType, ModalManager, type ProgressBarOptions, type ProgressBarRow, type RandomBotActivity, ReactionManager, type SelectMenuCreateOption, type SelectMenuList, SelectMenuManager, SimpleColor, SimpleDiscordBotInfo, type SparklineOptions, type SparklineRow, type SparklinesOptions, UserManager, WebhookManager };
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  BotEnv: () => BotEnv,
25
25
  ButtonManager: () => ButtonManager,
26
26
  CacheManager: () => import_utils22.CacheManager,
27
+ ChartManager: () => ChartManager,
27
28
  ComponentManager: () => ComponentManager,
28
29
  DiscordRegex: () => DiscordRegex,
29
30
  EmbedManager: () => EmbedManager,
@@ -763,7 +764,7 @@ var BotInteraction = class {
763
764
  // package.json
764
765
  var package_default = {
765
766
  name: "@spatulox/simplediscordbot",
766
- version: "3.0.2",
767
+ version: "3.1.0",
767
768
  author: "Spatulox",
768
769
  description: "Simple discord bot framework to set up a bot under 30 secondes",
769
770
  exports: {
@@ -2370,6 +2371,16 @@ var ComponentManager = class {
2370
2371
  });
2371
2372
  return container;
2372
2373
  }
2374
+ static chart(container, chart, separator = false) {
2375
+ const charts = Array.isArray(chart) ? chart : [chart];
2376
+ charts.forEach((c) => {
2377
+ container.addTextDisplayComponents(c);
2378
+ if (separator !== false) {
2379
+ container.addSeparatorComponents(this.separator(separator));
2380
+ }
2381
+ });
2382
+ return container;
2383
+ }
2373
2384
  /**
2374
2385
  * Add a media gallery (links)
2375
2386
  */
@@ -2465,30 +2476,135 @@ var ComponentManager = class {
2465
2476
  }
2466
2477
  };
2467
2478
 
2468
- // src/manager/interactible/ButtonManager.ts
2479
+ // src/manager/messages/ChartManager.ts
2469
2480
  var import_discord17 = require("discord.js");
2481
+ var SPARK_BLOCKS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
2482
+ var DEFAULT_MAX_POINTS = 50;
2483
+ var EMPTY = "\u2014";
2484
+ var ChartManager = class _ChartManager {
2485
+ /**
2486
+ * A single gauge : `CPU : ███░░░░░░░ 32.7 %`
2487
+ */
2488
+ static progressBar(label, value, max = 100, options = {}) {
2489
+ const line = `${label} : ${_ChartManager.renderBar(value, max, options)}`;
2490
+ return new import_discord17.TextDisplayBuilder().setContent(_ChartManager.wrap(line, options.codeBlock));
2491
+ }
2492
+ /**
2493
+ * Several gauges in ONE TextDisplay, so a dashboard of 6 metrics still costs 1 component.
2494
+ * Labels are padded to the longest one so the bars start at the same column.
2495
+ */
2496
+ static progressBars(rows, options = {}) {
2497
+ const width = rows.reduce((longest, row) => Math.max(longest, row.label.length), 0);
2498
+ const lines = rows.map((row) => {
2499
+ const opts = { ...options, ...row.options };
2500
+ return `${row.label.padEnd(width, " ")} : ${_ChartManager.renderBar(row.value, row.max ?? 100, opts)}`;
2501
+ });
2502
+ return new import_discord17.TextDisplayBuilder().setContent(_ChartManager.wrap(lines.join("\n"), options.codeBlock));
2503
+ }
2504
+ /**
2505
+ * A one line curve : `RAM : ▁▂▃▅▇█▆▄▃▂▁ 62 %`. One block character per point, the last
2506
+ * value is printed at the end because a sparkline alone carries no scale.
2507
+ */
2508
+ static sparkline(label, values, options = {}) {
2509
+ const line = `${label} : ${_ChartManager.renderSpark(values, options)}`;
2510
+ return new import_discord17.TextDisplayBuilder().setContent(_ChartManager.wrap(line, options.codeBlock));
2511
+ }
2512
+ /**
2513
+ * Several curves in ONE TextDisplay, the sparkline counterpart of progressBars().
2514
+ * Labels are padded to the longest one so every curve starts at the same column.
2515
+ */
2516
+ static sparklines(rows, options = {}) {
2517
+ const width = rows.reduce((longest, row) => Math.max(longest, row.label.length), 0);
2518
+ const base = _ChartManager.withSharedScale(rows, options);
2519
+ const lines = rows.map((row) => {
2520
+ const opts = { ...base, ...row.options };
2521
+ return `${row.label.padEnd(width, " ")} : ${_ChartManager.renderSpark(row.values, opts)}`;
2522
+ });
2523
+ return new import_discord17.TextDisplayBuilder().setContent(_ChartManager.wrap(lines.join("\n"), options.codeBlock));
2524
+ }
2525
+ /**
2526
+ * Only fills the bounds that were not given explicitly, and only when asked : a row
2527
+ * setting its own min/max still wins, since row options are spread after these.
2528
+ */
2529
+ static withSharedScale(rows, options) {
2530
+ if (!options.sharedScale) return options;
2531
+ if (options.min !== void 0 && options.max !== void 0) return options;
2532
+ const points = rows.flatMap((row) => row.values).filter((v) => Number.isFinite(v));
2533
+ if (points.length === 0) return options;
2534
+ return {
2535
+ ...options,
2536
+ min: options.min ?? Math.min(...points),
2537
+ max: options.max ?? Math.max(...points)
2538
+ };
2539
+ }
2540
+ static renderBar(value, max, options) {
2541
+ const width = Math.max(1, Math.trunc(options.width ?? 10));
2542
+ const filled = options.filled ?? "\u2588";
2543
+ const empty = options.empty ?? "\u2591";
2544
+ const ratio = _ChartManager.clamp01(value / max);
2545
+ const filledCount = Math.round(ratio * width);
2546
+ const bar = filled.repeat(filledCount) + empty.repeat(width - filledCount);
2547
+ return bar + _ChartManager.formatValue(value, options);
2548
+ }
2549
+ static renderSpark(values, options) {
2550
+ const maxPoints = Math.max(1, Math.trunc(options.maxPoints ?? DEFAULT_MAX_POINTS));
2551
+ const points = values.filter((v) => Number.isFinite(v)).slice(-maxPoints);
2552
+ if (points.length === 0) return EMPTY;
2553
+ const lo = options.min ?? Math.min(...points);
2554
+ const hi = options.max ?? Math.max(...points);
2555
+ const middle = SPARK_BLOCKS[Math.floor(SPARK_BLOCKS.length / 2) - 1];
2556
+ const curve = hi === lo ? middle.repeat(points.length) : points.map((v) => {
2557
+ const level = Math.round(_ChartManager.clamp01((v - lo) / (hi - lo)) * (SPARK_BLOCKS.length - 1));
2558
+ return SPARK_BLOCKS[level];
2559
+ }).join("");
2560
+ const last = points[points.length - 1];
2561
+ return curve + _ChartManager.formatValue(last, { ...options, unit: options.unit ?? "" });
2562
+ }
2563
+ static formatValue(value, options) {
2564
+ if (options.showValue === false) return "";
2565
+ const decimals = Math.max(0, Math.trunc(options.decimals ?? 1));
2566
+ const printable = Number.isFinite(value) ? value.toFixed(decimals) : "?";
2567
+ const unit = options.unit ?? "%";
2568
+ return unit ? ` ${printable} ${unit}` : ` ${printable}`;
2569
+ }
2570
+ /**
2571
+ * TextDisplayBuilder.setContent() rejects an empty string, so an empty row list must
2572
+ * still render something rather than throw at build time.
2573
+ */
2574
+ static wrap(content, codeBlock) {
2575
+ const body = content.length > 0 ? content : EMPTY;
2576
+ return codeBlock ? "```\n" + body + "\n```" : body;
2577
+ }
2578
+ static clamp01(ratio) {
2579
+ if (!Number.isFinite(ratio)) return 0;
2580
+ return Math.min(1, Math.max(0, ratio));
2581
+ }
2582
+ };
2583
+
2584
+ // src/manager/interactible/ButtonManager.ts
2585
+ var import_discord18 = require("discord.js");
2470
2586
  var ButtonManager = class _ButtonManager {
2471
2587
  static create(options) {
2472
- const btn = new import_discord17.ButtonBuilder().setCustomId(options.customId).setLabel(options.label ?? "Button").setStyle(options.style).setDisabled(options.disabled ?? false);
2588
+ const btn = new import_discord18.ButtonBuilder().setCustomId(options.customId).setLabel(options.label ?? "Button").setStyle(options.style).setDisabled(options.disabled ?? false);
2473
2589
  if (options.emoji) {
2474
2590
  btn.setEmoji(options.emoji);
2475
2591
  }
2476
2592
  return btn;
2477
2593
  }
2478
2594
  static primary(options) {
2479
- return this.create({ ...options, style: import_discord17.ButtonStyle.Primary });
2595
+ return this.create({ ...options, style: import_discord18.ButtonStyle.Primary });
2480
2596
  }
2481
2597
  static success(options) {
2482
- return this.create({ ...options, style: import_discord17.ButtonStyle.Success });
2598
+ return this.create({ ...options, style: import_discord18.ButtonStyle.Success });
2483
2599
  }
2484
2600
  static secondary(options) {
2485
- return this.create({ ...options, style: import_discord17.ButtonStyle.Secondary });
2601
+ return this.create({ ...options, style: import_discord18.ButtonStyle.Secondary });
2486
2602
  }
2487
2603
  static danger(options) {
2488
- return this.create({ ...options, style: import_discord17.ButtonStyle.Danger });
2604
+ return this.create({ ...options, style: import_discord18.ButtonStyle.Danger });
2489
2605
  }
2490
2606
  static link(options) {
2491
- const btn = new import_discord17.ButtonBuilder().setLabel(options.label).setStyle(import_discord17.ButtonStyle.Link).setURL(options.url);
2607
+ const btn = new import_discord18.ButtonBuilder().setLabel(options.label).setStyle(import_discord18.ButtonStyle.Link).setURL(options.url);
2492
2608
  if (options.emoji) btn.setEmoji(options.emoji);
2493
2609
  return btn;
2494
2610
  }
@@ -2500,7 +2616,7 @@ var ButtonManager = class _ButtonManager {
2500
2616
  }
2501
2617
  static row(but) {
2502
2618
  const buttons = Array.isArray(but) ? but.slice(0, 5) : [but];
2503
- return new import_discord17.ActionRowBuilder().addComponents(buttons);
2619
+ return new import_discord18.ActionRowBuilder().addComponents(buttons);
2504
2620
  }
2505
2621
  static toMessage(button) {
2506
2622
  return {
@@ -2510,7 +2626,7 @@ var ButtonManager = class _ButtonManager {
2510
2626
  static toInteraction(button, ephemeral = false) {
2511
2627
  return {
2512
2628
  components: this.createRowsToReturn(button),
2513
- flags: ephemeral ? [import_discord17.MessageFlags.Ephemeral] : []
2629
+ flags: ephemeral ? [import_discord18.MessageFlags.Ephemeral] : []
2514
2630
  };
2515
2631
  }
2516
2632
  static toInteractionEdit(button) {
@@ -2521,10 +2637,10 @@ var ButtonManager = class _ButtonManager {
2521
2637
  static createRowsToReturn(button) {
2522
2638
  if (Array.isArray(button)) {
2523
2639
  return button.map(
2524
- (btn) => btn instanceof import_discord17.ActionRowBuilder ? btn : _ButtonManager.row(btn)
2640
+ (btn) => btn instanceof import_discord18.ActionRowBuilder ? btn : _ButtonManager.row(btn)
2525
2641
  );
2526
2642
  }
2527
- return button instanceof import_discord17.ActionRowBuilder ? [button] : [_ButtonManager.row(button)];
2643
+ return button instanceof import_discord18.ActionRowBuilder ? [button] : [_ButtonManager.row(button)];
2528
2644
  }
2529
2645
  };
2530
2646
 
@@ -2652,6 +2768,7 @@ var DiscordRegex = class {
2652
2768
  BotEnv,
2653
2769
  ButtonManager,
2654
2770
  CacheManager,
2771
+ ChartManager,
2655
2772
  ComponentManager,
2656
2773
  DiscordRegex,
2657
2774
  EmbedManager,
package/dist/index.mjs CHANGED
@@ -743,7 +743,7 @@ var BotInteraction = class {
743
743
  // package.json
744
744
  var package_default = {
745
745
  name: "@spatulox/simplediscordbot",
746
- version: "3.0.2",
746
+ version: "3.1.0",
747
747
  author: "Spatulox",
748
748
  description: "Simple discord bot framework to set up a bot under 30 secondes",
749
749
  exports: {
@@ -2373,6 +2373,16 @@ var ComponentManager = class {
2373
2373
  });
2374
2374
  return container;
2375
2375
  }
2376
+ static chart(container, chart, separator = false) {
2377
+ const charts = Array.isArray(chart) ? chart : [chart];
2378
+ charts.forEach((c) => {
2379
+ container.addTextDisplayComponents(c);
2380
+ if (separator !== false) {
2381
+ container.addSeparatorComponents(this.separator(separator));
2382
+ }
2383
+ });
2384
+ return container;
2385
+ }
2376
2386
  /**
2377
2387
  * Add a media gallery (links)
2378
2388
  */
@@ -2468,6 +2478,111 @@ var ComponentManager = class {
2468
2478
  }
2469
2479
  };
2470
2480
 
2481
+ // src/manager/messages/ChartManager.ts
2482
+ import { TextDisplayBuilder as TextDisplayBuilder2 } from "discord.js";
2483
+ var SPARK_BLOCKS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
2484
+ var DEFAULT_MAX_POINTS = 50;
2485
+ var EMPTY = "\u2014";
2486
+ var ChartManager = class _ChartManager {
2487
+ /**
2488
+ * A single gauge : `CPU : ███░░░░░░░ 32.7 %`
2489
+ */
2490
+ static progressBar(label, value, max = 100, options = {}) {
2491
+ const line = `${label} : ${_ChartManager.renderBar(value, max, options)}`;
2492
+ return new TextDisplayBuilder2().setContent(_ChartManager.wrap(line, options.codeBlock));
2493
+ }
2494
+ /**
2495
+ * Several gauges in ONE TextDisplay, so a dashboard of 6 metrics still costs 1 component.
2496
+ * Labels are padded to the longest one so the bars start at the same column.
2497
+ */
2498
+ static progressBars(rows, options = {}) {
2499
+ const width = rows.reduce((longest, row) => Math.max(longest, row.label.length), 0);
2500
+ const lines = rows.map((row) => {
2501
+ const opts = { ...options, ...row.options };
2502
+ return `${row.label.padEnd(width, " ")} : ${_ChartManager.renderBar(row.value, row.max ?? 100, opts)}`;
2503
+ });
2504
+ return new TextDisplayBuilder2().setContent(_ChartManager.wrap(lines.join("\n"), options.codeBlock));
2505
+ }
2506
+ /**
2507
+ * A one line curve : `RAM : ▁▂▃▅▇█▆▄▃▂▁ 62 %`. One block character per point, the last
2508
+ * value is printed at the end because a sparkline alone carries no scale.
2509
+ */
2510
+ static sparkline(label, values, options = {}) {
2511
+ const line = `${label} : ${_ChartManager.renderSpark(values, options)}`;
2512
+ return new TextDisplayBuilder2().setContent(_ChartManager.wrap(line, options.codeBlock));
2513
+ }
2514
+ /**
2515
+ * Several curves in ONE TextDisplay, the sparkline counterpart of progressBars().
2516
+ * Labels are padded to the longest one so every curve starts at the same column.
2517
+ */
2518
+ static sparklines(rows, options = {}) {
2519
+ const width = rows.reduce((longest, row) => Math.max(longest, row.label.length), 0);
2520
+ const base = _ChartManager.withSharedScale(rows, options);
2521
+ const lines = rows.map((row) => {
2522
+ const opts = { ...base, ...row.options };
2523
+ return `${row.label.padEnd(width, " ")} : ${_ChartManager.renderSpark(row.values, opts)}`;
2524
+ });
2525
+ return new TextDisplayBuilder2().setContent(_ChartManager.wrap(lines.join("\n"), options.codeBlock));
2526
+ }
2527
+ /**
2528
+ * Only fills the bounds that were not given explicitly, and only when asked : a row
2529
+ * setting its own min/max still wins, since row options are spread after these.
2530
+ */
2531
+ static withSharedScale(rows, options) {
2532
+ if (!options.sharedScale) return options;
2533
+ if (options.min !== void 0 && options.max !== void 0) return options;
2534
+ const points = rows.flatMap((row) => row.values).filter((v) => Number.isFinite(v));
2535
+ if (points.length === 0) return options;
2536
+ return {
2537
+ ...options,
2538
+ min: options.min ?? Math.min(...points),
2539
+ max: options.max ?? Math.max(...points)
2540
+ };
2541
+ }
2542
+ static renderBar(value, max, options) {
2543
+ const width = Math.max(1, Math.trunc(options.width ?? 10));
2544
+ const filled = options.filled ?? "\u2588";
2545
+ const empty = options.empty ?? "\u2591";
2546
+ const ratio = _ChartManager.clamp01(value / max);
2547
+ const filledCount = Math.round(ratio * width);
2548
+ const bar = filled.repeat(filledCount) + empty.repeat(width - filledCount);
2549
+ return bar + _ChartManager.formatValue(value, options);
2550
+ }
2551
+ static renderSpark(values, options) {
2552
+ const maxPoints = Math.max(1, Math.trunc(options.maxPoints ?? DEFAULT_MAX_POINTS));
2553
+ const points = values.filter((v) => Number.isFinite(v)).slice(-maxPoints);
2554
+ if (points.length === 0) return EMPTY;
2555
+ const lo = options.min ?? Math.min(...points);
2556
+ const hi = options.max ?? Math.max(...points);
2557
+ const middle = SPARK_BLOCKS[Math.floor(SPARK_BLOCKS.length / 2) - 1];
2558
+ const curve = hi === lo ? middle.repeat(points.length) : points.map((v) => {
2559
+ const level = Math.round(_ChartManager.clamp01((v - lo) / (hi - lo)) * (SPARK_BLOCKS.length - 1));
2560
+ return SPARK_BLOCKS[level];
2561
+ }).join("");
2562
+ const last = points[points.length - 1];
2563
+ return curve + _ChartManager.formatValue(last, { ...options, unit: options.unit ?? "" });
2564
+ }
2565
+ static formatValue(value, options) {
2566
+ if (options.showValue === false) return "";
2567
+ const decimals = Math.max(0, Math.trunc(options.decimals ?? 1));
2568
+ const printable = Number.isFinite(value) ? value.toFixed(decimals) : "?";
2569
+ const unit = options.unit ?? "%";
2570
+ return unit ? ` ${printable} ${unit}` : ` ${printable}`;
2571
+ }
2572
+ /**
2573
+ * TextDisplayBuilder.setContent() rejects an empty string, so an empty row list must
2574
+ * still render something rather than throw at build time.
2575
+ */
2576
+ static wrap(content, codeBlock) {
2577
+ const body = content.length > 0 ? content : EMPTY;
2578
+ return codeBlock ? "```\n" + body + "\n```" : body;
2579
+ }
2580
+ static clamp01(ratio) {
2581
+ if (!Number.isFinite(ratio)) return 0;
2582
+ return Math.min(1, Math.max(0, ratio));
2583
+ }
2584
+ };
2585
+
2471
2586
  // src/manager/interactible/ButtonManager.ts
2472
2587
  import {
2473
2588
  ButtonBuilder as ButtonBuilder2,
@@ -2659,6 +2774,7 @@ export {
2659
2774
  BotEnv,
2660
2775
  ButtonManager,
2661
2776
  CacheManager,
2777
+ ChartManager,
2662
2778
  ComponentManager,
2663
2779
  DiscordRegex,
2664
2780
  EmbedManager,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spatulox/simplediscordbot",
3
- "version": "3.0.3",
3
+ "version": "3.1.1",
4
4
  "author": "Spatulox",
5
5
  "description": "Simple discord bot framework to set up a bot under 30 secondes",
6
6
  "exports": {