@developer.krd/discord-dashboard 0.1.2 → 0.1.3

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/dist/index.cjs CHANGED
@@ -31,99 +31,98 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  DashboardDesigner: () => DashboardDesigner,
34
+ DiscordDashboard: () => DiscordDashboard,
35
+ DiscordHelpers: () => DiscordHelpers,
34
36
  builtinTemplateRenderers: () => builtinTemplateRenderers,
35
- createDashboard: () => createDashboard,
36
- createDashboardDesigner: () => createDashboardDesigner,
37
- createDiscordHelpers: () => createDiscordHelpers,
38
37
  getBuiltinTemplateRenderer: () => getBuiltinTemplateRenderer
39
38
  });
40
39
  module.exports = __toCommonJS(index_exports);
41
-
42
- // src/dashboard.ts
43
40
  var import_compression = __toESM(require("compression"), 1);
44
41
  var import_express = __toESM(require("express"), 1);
45
42
  var import_express_session = __toESM(require("express-session"), 1);
46
43
  var import_helmet = __toESM(require("helmet"), 1);
47
- var import_node_http = require("http");
48
44
  var import_node_crypto = require("crypto");
45
+ var import_node_http = require("http");
49
46
 
50
- // src/discord-helpers.ts
51
- var DISCORD_API = "https://discord.com/api/v10";
52
- async function fetchDiscordWithBot(botToken, path) {
53
- const response = await fetch(`${DISCORD_API}${path}`, {
54
- headers: {
55
- Authorization: `Bot ${botToken}`
47
+ // src/handlers/DiscordHelpers.ts
48
+ var DiscordHelpers = class {
49
+ botToken;
50
+ DISCORD_API = "https://discord.com/api/v10";
51
+ constructor(botToken) {
52
+ this.botToken = botToken;
53
+ }
54
+ async fetchDiscordWithBot(path) {
55
+ const response = await fetch(`${this.DISCORD_API}${path}`, {
56
+ headers: {
57
+ Authorization: `Bot ${this.botToken}`
58
+ }
59
+ });
60
+ if (!response.ok) {
61
+ return null;
56
62
  }
57
- });
58
- if (!response.ok) {
59
- return null;
63
+ return await response.json();
60
64
  }
61
- return await response.json();
62
- }
63
- function createDiscordHelpers(botToken) {
64
- return {
65
- async getChannel(channelId) {
66
- return await fetchDiscordWithBot(botToken, `/channels/${channelId}`);
67
- },
68
- async getGuildChannels(guildId) {
69
- return await fetchDiscordWithBot(botToken, `/guilds/${guildId}/channels`) ?? [];
70
- },
71
- async searchGuildChannels(guildId, query, options) {
72
- const channels = await fetchDiscordWithBot(botToken, `/guilds/${guildId}/channels`) ?? [];
73
- const normalizedQuery = query.trim().toLowerCase();
74
- const limit = Math.max(1, Math.min(options?.limit ?? 10, 50));
75
- return channels.filter((channel) => {
76
- if (options?.nsfw !== void 0 && Boolean(channel.nsfw) !== options.nsfw) {
77
- return false;
78
- }
79
- if (options?.channelTypes && options.channelTypes.length > 0 && !options.channelTypes.includes(channel.type)) {
80
- return false;
81
- }
82
- if (!normalizedQuery) {
83
- return true;
84
- }
85
- return channel.name.toLowerCase().includes(normalizedQuery);
86
- }).slice(0, limit);
87
- },
88
- async getRole(guildId, roleId) {
89
- const roles = await fetchDiscordWithBot(botToken, `/guilds/${guildId}/roles`);
90
- if (!roles) {
91
- return null;
65
+ async getChannel(channelId) {
66
+ return await this.fetchDiscordWithBot(`/channels/${channelId}`);
67
+ }
68
+ async getGuildChannels(guildId) {
69
+ return await this.fetchDiscordWithBot(`/guilds/${guildId}/channels`) ?? [];
70
+ }
71
+ async searchGuildChannels(guildId, query, options) {
72
+ const channels = await this.fetchDiscordWithBot(`/guilds/${guildId}/channels`) ?? [];
73
+ const normalizedQuery = query.trim().toLowerCase();
74
+ const limit = Math.max(1, Math.min(options?.limit ?? 10, 50));
75
+ return channels.filter((channel) => {
76
+ if (options?.nsfw !== void 0 && Boolean(channel.nsfw) !== options.nsfw) {
77
+ return false;
92
78
  }
93
- return roles.find((role) => role.id === roleId) ?? null;
94
- },
95
- async getGuildRoles(guildId) {
96
- return await fetchDiscordWithBot(botToken, `/guilds/${guildId}/roles`) ?? [];
97
- },
98
- async searchGuildRoles(guildId, query, options) {
99
- const roles = await fetchDiscordWithBot(botToken, `/guilds/${guildId}/roles`) ?? [];
100
- const normalizedQuery = query.trim().toLowerCase();
101
- const limit = Math.max(1, Math.min(options?.limit ?? 10, 50));
102
- return roles.filter((role) => {
103
- if (!options?.includeManaged && role.managed) {
104
- return false;
105
- }
106
- if (!normalizedQuery) {
107
- return true;
108
- }
109
- return role.name.toLowerCase().includes(normalizedQuery);
110
- }).sort((a, b) => b.position - a.position).slice(0, limit);
111
- },
112
- async searchGuildMembers(guildId, query, options) {
113
- const limit = Math.max(1, Math.min(options?.limit ?? 10, 1e3));
114
- const params = new URLSearchParams({
115
- query: query.trim(),
116
- limit: String(limit)
117
- });
118
- return await fetchDiscordWithBot(botToken, `/guilds/${guildId}/members/search?${params.toString()}`) ?? [];
119
- },
120
- async getGuildMember(guildId, userId) {
121
- return await fetchDiscordWithBot(botToken, `/guilds/${guildId}/members/${userId}`);
79
+ if (options?.channelTypes && options.channelTypes.length > 0 && !options.channelTypes.includes(channel.type)) {
80
+ return false;
81
+ }
82
+ if (!normalizedQuery) {
83
+ return true;
84
+ }
85
+ return channel.name.toLowerCase().includes(normalizedQuery);
86
+ }).slice(0, limit);
87
+ }
88
+ async getRole(guildId, roleId) {
89
+ const roles = await this.fetchDiscordWithBot(`/guilds/${guildId}/roles`);
90
+ if (!roles) {
91
+ return null;
122
92
  }
123
- };
124
- }
93
+ return roles.find((role) => role.id === roleId) ?? null;
94
+ }
95
+ async getGuildRoles(guildId) {
96
+ return await this.fetchDiscordWithBot(`/guilds/${guildId}/roles`) ?? [];
97
+ }
98
+ async searchGuildRoles(guildId, query, options) {
99
+ const roles = await this.fetchDiscordWithBot(`/guilds/${guildId}/roles`) ?? [];
100
+ const normalizedQuery = query.trim().toLowerCase();
101
+ const limit = Math.max(1, Math.min(options?.limit ?? 10, 50));
102
+ return roles.filter((role) => {
103
+ if (!options?.includeManaged && role.managed) {
104
+ return false;
105
+ }
106
+ if (!normalizedQuery) {
107
+ return true;
108
+ }
109
+ return role.name.toLowerCase().includes(normalizedQuery);
110
+ }).sort((a, b) => b.position - a.position).slice(0, limit);
111
+ }
112
+ async searchGuildMembers(guildId, query, options) {
113
+ const limit = Math.max(1, Math.min(options?.limit ?? 10, 1e3));
114
+ const params = new URLSearchParams({
115
+ query: query.trim(),
116
+ limit: String(limit)
117
+ });
118
+ return await this.fetchDiscordWithBot(`/guilds/${guildId}/members/search?${params.toString()}`) ?? [];
119
+ }
120
+ async getGuildMember(guildId, userId) {
121
+ return await this.fetchDiscordWithBot(`/guilds/${guildId}/members/${userId}`);
122
+ }
123
+ };
125
124
 
126
- // src/templates.ts
125
+ // src/templates/templates.ts
127
126
  var appCss = `
128
127
  :root {
129
128
  color-scheme: dark;
@@ -177,7 +176,6 @@ body {
177
176
  font-weight: 700;
178
177
  display: grid;
179
178
  place-items: center;
180
- cursor: pointer;
181
179
  transition: border-radius .15s ease, background .15s ease, transform .15s ease;
182
180
  }
183
181
  .server-item:hover { border-radius: 16px; background: #404249; }
@@ -292,7 +290,6 @@ button {
292
290
  color: var(--text);
293
291
  border-radius: 8px;
294
292
  padding: 8px 12px;
295
- cursor: pointer;
296
293
  }
297
294
  button.primary {
298
295
  background: var(--primary);
@@ -448,7 +445,6 @@ button.danger { background: #3a1e27; border-color: rgba(255,107,107,.45); }
448
445
  .drag-handle {
449
446
  color: var(--muted);
450
447
  user-select: none;
451
- cursor: grab;
452
448
  font-size: 0.9rem;
453
449
  }
454
450
  .list-input {
@@ -462,6 +458,7 @@ button.danger { background: #3a1e27; border-color: rgba(255,107,107,.45); }
462
458
  justify-self: start;
463
459
  }
464
460
  .empty { color: var(--muted); font-size: 0.9rem; }
461
+ .cursor-pointer { cursor: pointer; }
465
462
  `;
466
463
  function escapeHtml(value) {
467
464
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#039;");
@@ -469,13 +466,15 @@ function escapeHtml(value) {
469
466
  function renderDashboardHtml(name, basePath, setupDesign) {
470
467
  const safeName = escapeHtml(name);
471
468
  const scriptData = JSON.stringify({ basePath, setupDesign: setupDesign ?? {} });
469
+ const customCssBlock = setupDesign?.customCss ? `
470
+ <style>${setupDesign.customCss}</style>` : "";
472
471
  return `<!DOCTYPE html>
473
472
  <html lang="en">
474
473
  <head>
475
474
  <meta charset="UTF-8" />
476
475
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
477
476
  <title>${safeName}</title>
478
- <style>${appCss}</style>
477
+ <style>${appCss}</style>${customCssBlock}
479
478
  </head>
480
479
  <body>
481
480
  <div class="layout">
@@ -492,8 +491,8 @@ function renderDashboardHtml(name, basePath, setupDesign) {
492
491
 
493
492
  <div class="container">
494
493
  <div class="main-tabs">
495
- <button id="tabHome" class="main-tab active">Home</button>
496
- <button id="tabPlugins" class="main-tab">Plugins</button>
494
+ <button id="tabHome" class="main-tab active cursor-pointer">Home</button>
495
+ <button id="tabPlugins" class="main-tab cursor-pointer">Plugins</button>
497
496
  </div>
498
497
 
499
498
  <section id="homeArea">
@@ -604,7 +603,9 @@ function renderDashboardHtml(name, basePath, setupDesign) {
604
603
  const makeButton = (action, pluginId, panelId, panelElement) => {
605
604
  const button = document.createElement("button");
606
605
  button.textContent = action.label;
607
- button.className = action.variant === "primary" ? "primary" : action.variant === "danger" ? "danger" : "";
606
+ const variantClass = action.variant === "primary" ? "primary" : action.variant === "danger" ? "danger" : "";
607
+ button.className = [variantClass, "cursor-pointer"].filter(Boolean).join(" ");
608
+
608
609
  button.addEventListener("click", async () => {
609
610
  button.disabled = true;
610
611
  try {
@@ -709,7 +710,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
709
710
  el.serverRail.innerHTML = "";
710
711
  items.forEach((item) => {
711
712
  const button = document.createElement("button");
712
- button.className = "server-item" + (item.id === state.selectedGuildId ? " active" : "");
713
+ button.className = "server-item cursor-pointer" + (item.id === state.selectedGuildId ? " active" : "");
713
714
  button.title = item.id && !item.botInGuild ? (item.name + " \u2022 Invite bot") : item.name;
714
715
 
715
716
  const activeIndicator = document.createElement("span");
@@ -746,8 +747,8 @@ function renderDashboardHtml(name, basePath, setupDesign) {
746
747
  const homeActive = state.activeMainTab === "home";
747
748
  el.homeArea.style.display = homeActive ? "block" : "none";
748
749
  el.pluginsArea.style.display = homeActive ? "none" : "block";
749
- el.tabHome.className = "main-tab" + (homeActive ? " active" : "");
750
- el.tabPlugins.className = "main-tab" + (!homeActive ? " active" : "");
750
+ el.tabHome.className = "main-tab cursor-pointer" + (homeActive ? " active" : "");
751
+ el.tabPlugins.className = "main-tab cursor-pointer" + (!homeActive ? " active" : "");
751
752
  };
752
753
 
753
754
  const updateContextLabel = () => {
@@ -769,7 +770,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
769
770
  el.homeCategories.innerHTML = "";
770
771
  state.homeCategories.forEach((category) => {
771
772
  const button = document.createElement("button");
772
- button.className = "home-category-btn" + (state.selectedHomeCategoryId === category.id ? " active" : "");
773
+ button.className = "home-category-btn cursor-pointer" + (state.selectedHomeCategoryId === category.id ? " active" : "");
773
774
  button.textContent = category.label;
774
775
  button.title = category.description || category.label;
775
776
  button.addEventListener("click", async () => {
@@ -830,7 +831,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
830
831
  itemsWrap.className = "list-items";
831
832
  const addButton = document.createElement("button");
832
833
  addButton.type = "button";
833
- addButton.className = "list-add";
834
+ addButton.className = "list-add cursor-pointer";
834
835
  addButton.textContent = "Add Button";
835
836
 
836
837
  const normalizeValues = () => {
@@ -846,7 +847,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
846
847
  row.draggable = true;
847
848
 
848
849
  const handle = document.createElement("span");
849
- handle.className = "drag-handle";
850
+ handle.className = "drag-handle cursor-pointer";
850
851
  handle.textContent = "\u22EE\u22EE";
851
852
 
852
853
  const textInput = document.createElement("input");
@@ -858,6 +859,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
858
859
 
859
860
  const removeButton = document.createElement("button");
860
861
  removeButton.type = "button";
862
+ removeButton.className = "cursor-pointer";
861
863
  removeButton.textContent = "\xD7";
862
864
  removeButton.addEventListener("click", () => {
863
865
  row.remove();
@@ -935,7 +937,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
935
937
  items.forEach((item) => {
936
938
  const btn = document.createElement("button");
937
939
  btn.type = "button";
938
- btn.className = "lookup-item";
940
+ btn.className = "lookup-item cursor-pointer";
939
941
  btn.textContent = labelResolver(item);
940
942
  btn.addEventListener("click", () => {
941
943
  onSelect(item);
@@ -1091,7 +1093,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
1091
1093
  input.value = field.value == null ? "" : String(field.value);
1092
1094
  } else if (field.type === "select") {
1093
1095
  input = document.createElement("select");
1094
- input.className = "home-select";
1096
+ input.className = "home-select cursor-pointer";
1095
1097
  (field.options || []).forEach((option) => {
1096
1098
  const optionEl = document.createElement("option");
1097
1099
  optionEl.value = option.value;
@@ -1106,7 +1108,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
1106
1108
  row.className = "home-field-row";
1107
1109
  input = document.createElement("input");
1108
1110
  input.type = "checkbox";
1109
- input.className = "home-checkbox";
1111
+ input.className = "home-checkbox cursor-pointer";
1110
1112
  input.checked = Boolean(field.value);
1111
1113
  const stateText = document.createElement("span");
1112
1114
  stateText.textContent = input.checked ? "Enabled" : "Disabled";
@@ -1161,7 +1163,9 @@ function renderDashboardHtml(name, basePath, setupDesign) {
1161
1163
  section.actions.forEach((action) => {
1162
1164
  const button = document.createElement("button");
1163
1165
  button.textContent = action.label;
1164
- button.className = action.variant === "primary" ? "primary" : action.variant === "danger" ? "danger" : "";
1166
+ const variantClass = action.variant === "primary" ? "primary" : action.variant === "danger" ? "danger" : "";
1167
+ button.className = [variantClass, "cursor-pointer"].filter(Boolean).join(" ");
1168
+
1165
1169
  button.addEventListener("click", async () => {
1166
1170
  button.disabled = true;
1167
1171
  try {
@@ -1275,7 +1279,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
1275
1279
  input.value = field.value == null ? "" : String(field.value);
1276
1280
  } else if (field.type === "select") {
1277
1281
  input = document.createElement("select");
1278
- input.className = "home-select";
1282
+ input.className = "home-select cursor-pointer";
1279
1283
  (field.options || []).forEach((option) => {
1280
1284
  const optionEl = document.createElement("option");
1281
1285
  optionEl.value = option.value;
@@ -1290,7 +1294,7 @@ function renderDashboardHtml(name, basePath, setupDesign) {
1290
1294
  row.className = "home-field-row";
1291
1295
  input = document.createElement("input");
1292
1296
  input.type = "checkbox";
1293
- input.className = "home-checkbox";
1297
+ input.className = "home-checkbox cursor-pointer";
1294
1298
  input.checked = Boolean(field.value);
1295
1299
  const stateText = document.createElement("span");
1296
1300
  stateText.textContent = input.checked ? "Enabled" : "Disabled";
@@ -1500,7 +1504,6 @@ body {
1500
1504
  background: var(--panel);
1501
1505
  color: #fff;
1502
1506
  font-weight: 700;
1503
- cursor: pointer;
1504
1507
  transition: transform .15s ease, background .15s ease;
1505
1508
  }
1506
1509
  .server-item:hover { transform: translateY(-1px); background: #323b5f; }
@@ -1551,7 +1554,6 @@ button {
1551
1554
  color: var(--text);
1552
1555
  border-radius: 8px;
1553
1556
  padding: 7px 10px;
1554
- cursor: pointer;
1555
1557
  }
1556
1558
  button.primary { background: var(--primary); border: none; }
1557
1559
  button.danger { background: #4a2230; border-color: rgba(255,111,145,.45); }
@@ -1642,29 +1644,28 @@ button.danger { background: #4a2230; border-color: rgba(255,111,145,.45); }
1642
1644
  background: var(--panel);
1643
1645
  }
1644
1646
  .list-item.dragging { opacity: .6; }
1645
- .drag-handle { color: var(--muted); user-select: none; cursor: grab; font-size: .9rem; }
1647
+ .drag-handle { color: var(--muted); user-select: none; font-size: .9rem; }
1646
1648
  .list-input { width: 100%; border: none; outline: none; background: transparent; color: var(--text); }
1647
1649
  .list-add { justify-self: start; }
1648
1650
  .empty { color: var(--muted); font-size: .9rem; }
1651
+ .cursor-pointer { cursor: pointer; }
1649
1652
  @media (max-width: 980px) {
1650
1653
  .layout { grid-template-columns: 70px 1fr; }
1651
1654
  .home-width-50, .home-width-33, .home-width-20 { flex-basis: 100%; max-width: 100%; }
1652
1655
  }
1653
1656
  `;
1654
- var compactDashboardTemplateRenderer = ({
1655
- dashboardName,
1656
- basePath,
1657
- setupDesign
1658
- }) => {
1657
+ var compactDashboardTemplateRenderer = ({ dashboardName, basePath, setupDesign }) => {
1659
1658
  const script = extractDashboardScript(renderDashboardHtml(dashboardName, basePath, setupDesign));
1660
1659
  const safeName = escapeHtml2(dashboardName);
1660
+ const customCssBlock = setupDesign?.customCss ? `
1661
+ <style>${setupDesign.customCss}</style>` : "";
1661
1662
  return `<!DOCTYPE html>
1662
1663
  <html lang="en">
1663
1664
  <head>
1664
1665
  <meta charset="UTF-8" />
1665
1666
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1666
1667
  <title>${safeName}</title>
1667
- <style>${compactCss}</style>
1668
+ <style>${compactCss}</style>${customCssBlock}
1668
1669
  </head>
1669
1670
  <body>
1670
1671
  <div class="shell">
@@ -1682,8 +1683,8 @@ var compactDashboardTemplateRenderer = ({
1682
1683
  <main class="content">
1683
1684
  <div class="container">
1684
1685
  <div class="main-tabs">
1685
- <button id="tabHome" class="main-tab active">Home</button>
1686
- <button id="tabPlugins" class="main-tab">Plugins</button>
1686
+ <button id="tabHome" class="main-tab active cursor-pointer">Home</button>
1687
+ <button id="tabPlugins" class="main-tab cursor-pointer">Plugins</button>
1687
1688
  </div>
1688
1689
 
1689
1690
  <section id="homeArea">
@@ -1712,11 +1713,7 @@ var compactDashboardTemplateRenderer = ({
1712
1713
  };
1713
1714
 
1714
1715
  // src/templates/default.ts
1715
- var defaultDashboardTemplateRenderer = ({
1716
- dashboardName,
1717
- basePath,
1718
- setupDesign
1719
- }) => renderDashboardHtml(dashboardName, basePath, setupDesign);
1716
+ var defaultDashboardTemplateRenderer = ({ dashboardName, basePath, setupDesign }) => renderDashboardHtml(dashboardName, basePath, setupDesign);
1720
1717
 
1721
1718
  // src/templates/index.ts
1722
1719
  var builtinTemplateRenderers = {
@@ -1727,565 +1724,7 @@ function getBuiltinTemplateRenderer(templateId) {
1727
1724
  return builtinTemplateRenderers[templateId];
1728
1725
  }
1729
1726
 
1730
- // src/dashboard.ts
1731
- var DISCORD_API2 = "https://discord.com/api/v10";
1732
- var MANAGE_GUILD_PERMISSION = 0x20n;
1733
- var ADMIN_PERMISSION = 0x8n;
1734
- function normalizeBasePath(basePath) {
1735
- if (!basePath || basePath === "/") {
1736
- return "/dashboard";
1737
- }
1738
- return basePath.startsWith("/") ? basePath : `/${basePath}`;
1739
- }
1740
- function canManageGuild(permissions) {
1741
- const value = BigInt(permissions);
1742
- return (value & MANAGE_GUILD_PERMISSION) === MANAGE_GUILD_PERMISSION || (value & ADMIN_PERMISSION) === ADMIN_PERMISSION;
1743
- }
1744
- function toQuery(params) {
1745
- const url = new URLSearchParams();
1746
- for (const [key, value] of Object.entries(params)) {
1747
- url.set(key, value);
1748
- }
1749
- return url.toString();
1750
- }
1751
- async function fetchDiscord(path, token) {
1752
- const response = await fetch(`${DISCORD_API2}${path}`, {
1753
- headers: {
1754
- Authorization: `Bearer ${token}`
1755
- }
1756
- });
1757
- if (!response.ok) {
1758
- throw new Error(`Discord API request failed (${response.status})`);
1759
- }
1760
- return await response.json();
1761
- }
1762
- async function exchangeCodeForToken(options, code) {
1763
- const response = await fetch(`${DISCORD_API2}/oauth2/token`, {
1764
- method: "POST",
1765
- headers: {
1766
- "Content-Type": "application/x-www-form-urlencoded"
1767
- },
1768
- body: toQuery({
1769
- client_id: options.clientId,
1770
- client_secret: options.clientSecret,
1771
- grant_type: "authorization_code",
1772
- code,
1773
- redirect_uri: options.redirectUri
1774
- })
1775
- });
1776
- if (!response.ok) {
1777
- const text = await response.text();
1778
- throw new Error(`Failed token exchange: ${response.status} ${text}`);
1779
- }
1780
- return await response.json();
1781
- }
1782
- function createContext(req, options) {
1783
- const auth = req.session.discordAuth;
1784
- if (!auth) {
1785
- throw new Error("Not authenticated");
1786
- }
1787
- const selectedGuildId = typeof req.query.guildId === "string" ? req.query.guildId : void 0;
1788
- return {
1789
- user: auth.user,
1790
- guilds: auth.guilds,
1791
- accessToken: auth.accessToken,
1792
- selectedGuildId,
1793
- helpers: createDiscordHelpers(options.botToken)
1794
- };
1795
- }
1796
- function ensureAuthenticated(req, res, next) {
1797
- if (!req.session.discordAuth) {
1798
- res.status(401).json({ authenticated: false, message: "Authentication required" });
1799
- return;
1800
- }
1801
- next();
1802
- }
1803
- async function resolveOverviewCards(options, context) {
1804
- if (options.getOverviewCards) {
1805
- return await options.getOverviewCards(context);
1806
- }
1807
- const manageableGuildCount = context.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions)).length;
1808
- return [
1809
- {
1810
- id: "user",
1811
- title: "Logged-in User",
1812
- value: context.user.global_name || context.user.username,
1813
- subtitle: `ID: ${context.user.id}`,
1814
- intent: "info"
1815
- },
1816
- {
1817
- id: "guilds",
1818
- title: "Manageable Guilds",
1819
- value: manageableGuildCount,
1820
- subtitle: "Owner or Manage Server permissions",
1821
- intent: "success"
1822
- },
1823
- {
1824
- id: "plugins",
1825
- title: "Plugins Loaded",
1826
- value: options.plugins?.length ?? 0,
1827
- subtitle: "Dynamic server modules",
1828
- intent: "neutral"
1829
- }
1830
- ];
1831
- }
1832
- async function resolveHomeSections(options, context) {
1833
- const customSections = options.home?.getSections ? await options.home.getSections(context) : [];
1834
- const overviewSections = options.home?.getOverviewSections ? await options.home.getOverviewSections(context) : [];
1835
- if (customSections.length > 0 || overviewSections.length > 0) {
1836
- const normalizedOverview = overviewSections.map((section) => ({
1837
- ...section,
1838
- categoryId: section.categoryId ?? "overview"
1839
- }));
1840
- return [...normalizedOverview, ...customSections];
1841
- }
1842
- const selectedGuild = context.selectedGuildId ? context.guilds.find((guild) => guild.id === context.selectedGuildId) : void 0;
1843
- return [
1844
- {
1845
- id: "setup",
1846
- title: "Setup Details",
1847
- description: "Core dashboard setup information",
1848
- scope: "setup",
1849
- categoryId: "setup",
1850
- fields: [
1851
- {
1852
- id: "dashboardName",
1853
- label: "Dashboard Name",
1854
- type: "text",
1855
- value: options.dashboardName ?? "Discord Dashboard",
1856
- readOnly: true
1857
- },
1858
- {
1859
- id: "basePath",
1860
- label: "Base Path",
1861
- type: "text",
1862
- value: options.basePath ?? "/dashboard",
1863
- readOnly: true
1864
- }
1865
- ]
1866
- },
1867
- {
1868
- id: "context",
1869
- title: "Dashboard Context",
1870
- description: selectedGuild ? `Managing ${selectedGuild.name}` : "Managing user dashboard",
1871
- scope: resolveScope(context),
1872
- categoryId: "overview",
1873
- fields: [
1874
- {
1875
- id: "mode",
1876
- label: "Mode",
1877
- type: "text",
1878
- value: selectedGuild ? "Guild" : "User",
1879
- readOnly: true
1880
- },
1881
- {
1882
- id: "target",
1883
- label: "Target",
1884
- type: "text",
1885
- value: selectedGuild ? selectedGuild.name : context.user.username,
1886
- readOnly: true
1887
- }
1888
- ]
1889
- }
1890
- ];
1891
- }
1892
- function resolveScope(context) {
1893
- return context.selectedGuildId ? "guild" : "user";
1894
- }
1895
- async function resolveHomeCategories(options, context) {
1896
- if (options.home?.getCategories) {
1897
- const categories = await options.home.getCategories(context);
1898
- return [...categories].sort((a, b) => {
1899
- if (a.id === "overview") return -1;
1900
- if (b.id === "overview") return 1;
1901
- return 0;
1902
- });
1903
- }
1904
- return [
1905
- { id: "overview", label: "Overview", scope: resolveScope(context) },
1906
- { id: "setup", label: "Setup", scope: "setup" }
1907
- ];
1908
- }
1909
- function getUserAvatarUrl(user) {
1910
- if (user.avatar) {
1911
- const ext = user.avatar.startsWith("a_") ? "gif" : "png";
1912
- return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${ext}?size=256`;
1913
- }
1914
- const fallbackIndex = Number((BigInt(user.id) >> 22n) % 6n);
1915
- return `https://cdn.discordapp.com/embed/avatars/${fallbackIndex}.png`;
1916
- }
1917
- function getGuildIconUrl(guild) {
1918
- if (!guild.icon) {
1919
- return null;
1920
- }
1921
- const ext = guild.icon.startsWith("a_") ? "gif" : "png";
1922
- return `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.${ext}?size=128`;
1923
- }
1924
- function createGuildInviteUrl(options, guildId) {
1925
- const scopes = options.botInviteScopes && options.botInviteScopes.length > 0 ? options.botInviteScopes : ["bot", "applications.commands"];
1926
- return `https://discord.com/oauth2/authorize?${toQuery({
1927
- client_id: options.clientId,
1928
- scope: scopes.join(" "),
1929
- permissions: options.botInvitePermissions ?? "8",
1930
- guild_id: guildId,
1931
- disable_guild_select: "true"
1932
- })}`;
1933
- }
1934
- async function fetchBotGuildIds(botToken) {
1935
- const response = await fetch(`${DISCORD_API2}/users/@me/guilds`, {
1936
- headers: {
1937
- Authorization: `Bot ${botToken}`
1938
- }
1939
- });
1940
- if (!response.ok) {
1941
- return /* @__PURE__ */ new Set();
1942
- }
1943
- const guilds = await response.json();
1944
- return new Set(guilds.map((guild) => guild.id));
1945
- }
1946
- function resolveTemplateRenderer(options) {
1947
- const selectedTemplate = options.uiTemplate ?? "default";
1948
- const defaultRenderer = ({ dashboardName, basePath, setupDesign }) => renderDashboardHtml(dashboardName, basePath, setupDesign);
1949
- const customRenderer = options.uiTemplates?.[selectedTemplate];
1950
- if (customRenderer) {
1951
- return customRenderer;
1952
- }
1953
- const builtinRenderer = getBuiltinTemplateRenderer(selectedTemplate);
1954
- if (builtinRenderer) {
1955
- return builtinRenderer;
1956
- }
1957
- if (selectedTemplate !== "default") {
1958
- throw new Error(`Unknown uiTemplate '${selectedTemplate}'. Register it in uiTemplates.`);
1959
- }
1960
- return defaultRenderer;
1961
- }
1962
- function createDashboard(options) {
1963
- const app = options.app ?? (0, import_express.default)();
1964
- const basePath = normalizeBasePath(options.basePath);
1965
- const dashboardName = options.dashboardName ?? "Discord Dashboard";
1966
- const templateRenderer = resolveTemplateRenderer(options);
1967
- const plugins = options.plugins ?? [];
1968
- if (!options.botToken) throw new Error("botToken is required");
1969
- if (!options.clientId) throw new Error("clientId is required");
1970
- if (!options.clientSecret) throw new Error("clientSecret is required");
1971
- if (!options.redirectUri) throw new Error("redirectUri is required");
1972
- if (!options.sessionSecret) throw new Error("sessionSecret is required");
1973
- if (!options.app && options.trustProxy !== void 0) {
1974
- app.set("trust proxy", options.trustProxy);
1975
- }
1976
- const router = import_express.default.Router();
1977
- const sessionMiddleware = (0, import_express_session.default)({
1978
- name: options.sessionName ?? "discord_dashboard.sid",
1979
- secret: options.sessionSecret,
1980
- resave: false,
1981
- saveUninitialized: false,
1982
- cookie: {
1983
- httpOnly: true,
1984
- sameSite: "lax",
1985
- maxAge: options.sessionMaxAgeMs ?? 1e3 * 60 * 60 * 24 * 7
1986
- }
1987
- });
1988
- router.use((0, import_compression.default)());
1989
- router.use(
1990
- (0, import_helmet.default)({
1991
- contentSecurityPolicy: false
1992
- })
1993
- );
1994
- router.use(import_express.default.json());
1995
- router.use(sessionMiddleware);
1996
- router.get("/", (req, res) => {
1997
- if (!req.session.discordAuth) {
1998
- res.redirect(`${basePath}/login`);
1999
- return;
2000
- }
2001
- res.setHeader("Cache-Control", "no-store");
2002
- res.type("html").send(templateRenderer({
2003
- dashboardName,
2004
- basePath,
2005
- setupDesign: options.setupDesign
2006
- }));
2007
- });
2008
- router.get("/login", (req, res) => {
2009
- const state = (0, import_node_crypto.randomBytes)(16).toString("hex");
2010
- req.session.oauthState = state;
2011
- const scope = (options.scopes && options.scopes.length > 0 ? options.scopes : ["identify", "guilds"]).join(" ");
2012
- const query = toQuery({
2013
- client_id: options.clientId,
2014
- redirect_uri: options.redirectUri,
2015
- response_type: "code",
2016
- scope,
2017
- state,
2018
- prompt: "none"
2019
- });
2020
- res.redirect(`https://discord.com/oauth2/authorize?${query}`);
2021
- });
2022
- router.get("/callback", async (req, res) => {
2023
- try {
2024
- const code = typeof req.query.code === "string" ? req.query.code : void 0;
2025
- const state = typeof req.query.state === "string" ? req.query.state : void 0;
2026
- if (!code || !state) {
2027
- res.status(400).send("Missing OAuth2 code/state");
2028
- return;
2029
- }
2030
- if (!req.session.oauthState || req.session.oauthState !== state) {
2031
- res.status(403).send("Invalid OAuth2 state");
2032
- return;
2033
- }
2034
- const tokenData = await exchangeCodeForToken(options, code);
2035
- const [user, guilds] = await Promise.all([
2036
- fetchDiscord("/users/@me", tokenData.access_token),
2037
- fetchDiscord("/users/@me/guilds", tokenData.access_token)
2038
- ]);
2039
- req.session.discordAuth = {
2040
- accessToken: tokenData.access_token,
2041
- refreshToken: tokenData.refresh_token,
2042
- expiresAt: tokenData.expires_in ? Date.now() + tokenData.expires_in * 1e3 : void 0,
2043
- user,
2044
- guilds
2045
- };
2046
- req.session.oauthState = void 0;
2047
- res.redirect(basePath);
2048
- } catch (error) {
2049
- const message = error instanceof Error ? error.message : "OAuth callback failed";
2050
- res.status(500).send(message);
2051
- }
2052
- });
2053
- router.post("/logout", (req, res) => {
2054
- req.session.destroy((sessionError) => {
2055
- if (sessionError) {
2056
- res.status(500).json({ ok: false, message: "Failed to destroy session" });
2057
- return;
2058
- }
2059
- res.clearCookie(options.sessionName ?? "discord_dashboard.sid");
2060
- res.json({ ok: true });
2061
- });
2062
- });
2063
- router.get("/api/session", (req, res) => {
2064
- const auth = req.session.discordAuth;
2065
- if (!auth) {
2066
- res.status(200).json({ authenticated: false });
2067
- return;
2068
- }
2069
- const manageableGuildCount = auth.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions)).length;
2070
- res.json({
2071
- authenticated: true,
2072
- user: {
2073
- ...auth.user,
2074
- avatarUrl: getUserAvatarUrl(auth.user)
2075
- },
2076
- guildCount: manageableGuildCount,
2077
- expiresAt: auth.expiresAt
2078
- });
2079
- });
2080
- router.get("/api/guilds", ensureAuthenticated, async (req, res) => {
2081
- const context = createContext(req, options);
2082
- if (options.ownerIds && options.ownerIds.length > 0 && !options.ownerIds.includes(context.user.id)) {
2083
- res.status(403).json({ message: "You are not allowed to access this dashboard." });
2084
- return;
2085
- }
2086
- let manageableGuilds = context.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions));
2087
- if (options.guildFilter) {
2088
- const filtered = [];
2089
- for (const guild of manageableGuilds) {
2090
- const allowed = await options.guildFilter(guild, context);
2091
- if (allowed) {
2092
- filtered.push(guild);
2093
- }
2094
- }
2095
- manageableGuilds = filtered;
2096
- }
2097
- const botGuildIds = await fetchBotGuildIds(options.botToken);
2098
- const enrichedGuilds = manageableGuilds.map((guild) => {
2099
- const botInGuild = botGuildIds.has(guild.id);
2100
- return {
2101
- ...guild,
2102
- iconUrl: getGuildIconUrl(guild),
2103
- botInGuild,
2104
- inviteUrl: botInGuild ? void 0 : createGuildInviteUrl(options, guild.id)
2105
- };
2106
- });
2107
- res.json({ guilds: enrichedGuilds });
2108
- });
2109
- router.get("/api/overview", ensureAuthenticated, async (req, res) => {
2110
- const context = createContext(req, options);
2111
- const cards = await resolveOverviewCards(options, context);
2112
- res.json({ cards });
2113
- });
2114
- router.get("/api/home/categories", ensureAuthenticated, async (req, res) => {
2115
- const context = createContext(req, options);
2116
- const activeScope = resolveScope(context);
2117
- const categories = await resolveHomeCategories(options, context);
2118
- const visible = categories.filter((item) => item.scope === "setup" || item.scope === activeScope);
2119
- res.json({ categories: visible, activeScope });
2120
- });
2121
- router.get("/api/home", ensureAuthenticated, async (req, res) => {
2122
- const context = createContext(req, options);
2123
- const activeScope = resolveScope(context);
2124
- const categoryId = typeof req.query.categoryId === "string" ? req.query.categoryId : void 0;
2125
- let sections = await resolveHomeSections(options, context);
2126
- sections = sections.filter((section) => {
2127
- const sectionScope = section.scope ?? activeScope;
2128
- if (sectionScope !== "setup" && sectionScope !== activeScope) {
2129
- return false;
2130
- }
2131
- if (!categoryId) {
2132
- return true;
2133
- }
2134
- return section.categoryId === categoryId;
2135
- });
2136
- res.json({ sections, activeScope });
2137
- });
2138
- router.get("/api/lookup/roles", ensureAuthenticated, async (req, res) => {
2139
- const context = createContext(req, options);
2140
- const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
2141
- if (!guildId) {
2142
- res.status(400).json({ message: "guildId is required" });
2143
- return;
2144
- }
2145
- const query = typeof req.query.q === "string" ? req.query.q : "";
2146
- const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
2147
- const includeManaged = typeof req.query.includeManaged === "string" ? req.query.includeManaged === "true" : void 0;
2148
- const roles = await context.helpers.searchGuildRoles(guildId, query, {
2149
- limit: Number.isFinite(limit) ? limit : void 0,
2150
- includeManaged
2151
- });
2152
- res.json({ roles });
2153
- });
2154
- router.get("/api/lookup/channels", ensureAuthenticated, async (req, res) => {
2155
- const context = createContext(req, options);
2156
- const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
2157
- if (!guildId) {
2158
- res.status(400).json({ message: "guildId is required" });
2159
- return;
2160
- }
2161
- const query = typeof req.query.q === "string" ? req.query.q : "";
2162
- const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
2163
- const nsfw = typeof req.query.nsfw === "string" ? req.query.nsfw === "true" : void 0;
2164
- const channelTypes = typeof req.query.channelTypes === "string" ? req.query.channelTypes.split(",").map((item) => Number(item.trim())).filter((item) => Number.isFinite(item)) : void 0;
2165
- const channels = await context.helpers.searchGuildChannels(guildId, query, {
2166
- limit: Number.isFinite(limit) ? limit : void 0,
2167
- nsfw,
2168
- channelTypes
2169
- });
2170
- res.json({ channels });
2171
- });
2172
- router.get("/api/lookup/members", ensureAuthenticated, async (req, res) => {
2173
- const context = createContext(req, options);
2174
- const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
2175
- if (!guildId) {
2176
- res.status(400).json({ message: "guildId is required" });
2177
- return;
2178
- }
2179
- const query = typeof req.query.q === "string" ? req.query.q : "";
2180
- const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
2181
- const members = await context.helpers.searchGuildMembers(guildId, query, {
2182
- limit: Number.isFinite(limit) ? limit : void 0
2183
- });
2184
- res.json({ members });
2185
- });
2186
- router.post("/api/home/:actionId", ensureAuthenticated, async (req, res) => {
2187
- const context = createContext(req, options);
2188
- const action = options.home?.actions?.[req.params.actionId];
2189
- if (!action) {
2190
- res.status(404).json({ ok: false, message: "Home action not found" });
2191
- return;
2192
- }
2193
- const payload = req.body;
2194
- if (!payload || typeof payload.sectionId !== "string" || !payload.values || typeof payload.values !== "object") {
2195
- res.status(400).json({ ok: false, message: "Invalid home action payload" });
2196
- return;
2197
- }
2198
- let result;
2199
- try {
2200
- result = await action(context, {
2201
- sectionId: payload.sectionId,
2202
- values: payload.values
2203
- });
2204
- } catch (error) {
2205
- const message = error instanceof Error ? error.message : "Home action failed";
2206
- res.status(500).json({ ok: false, message });
2207
- return;
2208
- }
2209
- res.json(result);
2210
- });
2211
- router.get("/api/plugins", ensureAuthenticated, async (req, res) => {
2212
- const context = createContext(req, options);
2213
- const activeScope = context.selectedGuildId ? "guild" : "user";
2214
- const payload = [];
2215
- for (const plugin of plugins) {
2216
- const pluginScope = plugin.scope ?? "both";
2217
- if (pluginScope !== "both" && pluginScope !== activeScope) {
2218
- continue;
2219
- }
2220
- const panels = await plugin.getPanels(context);
2221
- payload.push({
2222
- id: plugin.id,
2223
- name: plugin.name,
2224
- description: plugin.description,
2225
- panels
2226
- });
2227
- }
2228
- res.json({ plugins: payload });
2229
- });
2230
- router.post("/api/plugins/:pluginId/:actionId", ensureAuthenticated, async (req, res) => {
2231
- const context = createContext(req, options);
2232
- const plugin = plugins.find((item) => item.id === req.params.pluginId);
2233
- if (!plugin) {
2234
- res.status(404).json({ ok: false, message: "Plugin not found" });
2235
- return;
2236
- }
2237
- const action = plugin.actions?.[req.params.actionId];
2238
- if (!action) {
2239
- res.status(404).json({ ok: false, message: "Action not found" });
2240
- return;
2241
- }
2242
- let result;
2243
- try {
2244
- result = await action(context, req.body);
2245
- } catch (error) {
2246
- const message = error instanceof Error ? error.message : "Plugin action failed";
2247
- res.status(500).json({ ok: false, message });
2248
- return;
2249
- }
2250
- res.json(result);
2251
- });
2252
- app.use(basePath, router);
2253
- let server;
2254
- return {
2255
- app,
2256
- async start() {
2257
- if (options.app) {
2258
- return;
2259
- }
2260
- if (server) {
2261
- return;
2262
- }
2263
- const port = options.port ?? 3e3;
2264
- const host = options.host ?? "0.0.0.0";
2265
- server = (0, import_node_http.createServer)(app);
2266
- await new Promise((resolve) => {
2267
- server.listen(port, host, () => resolve());
2268
- });
2269
- },
2270
- async stop() {
2271
- if (!server) {
2272
- return;
2273
- }
2274
- await new Promise((resolve, reject) => {
2275
- server.close((error) => {
2276
- if (error) {
2277
- reject(error);
2278
- return;
2279
- }
2280
- resolve();
2281
- });
2282
- });
2283
- server = void 0;
2284
- }
2285
- };
2286
- }
2287
-
2288
- // src/designer.ts
1727
+ // src/handlers/DashboardDesigner.ts
2289
1728
  var CategoryBuilder = class {
2290
1729
  constructor(scope, categoryId, categoryLabel) {
2291
1730
  this.scope = scope;
@@ -2307,11 +1746,7 @@ var CategoryBuilder = class {
2307
1746
  return this;
2308
1747
  }
2309
1748
  buildCategory() {
2310
- return {
2311
- id: this.categoryId,
2312
- label: this.categoryLabel,
2313
- scope: this.scope
2314
- };
1749
+ return { id: this.categoryId, label: this.categoryLabel, scope: this.scope };
2315
1750
  }
2316
1751
  buildSections() {
2317
1752
  return [...this.sections];
@@ -2328,12 +1763,7 @@ var DashboardDesigner = class {
2328
1763
  this.partialOptions = { ...baseOptions };
2329
1764
  }
2330
1765
  setup(input) {
2331
- if (input.ownerIds) this.partialOptions.ownerIds = input.ownerIds;
2332
- if (input.botInvitePermissions) this.partialOptions.botInvitePermissions = input.botInvitePermissions;
2333
- if (input.botInviteScopes) this.partialOptions.botInviteScopes = input.botInviteScopes;
2334
- if (input.dashboardName) this.partialOptions.dashboardName = input.dashboardName;
2335
- if (input.basePath) this.partialOptions.basePath = input.basePath;
2336
- if (input.uiTemplate) this.partialOptions.uiTemplate = input.uiTemplate;
1766
+ Object.assign(this.partialOptions, input);
2337
1767
  return this;
2338
1768
  }
2339
1769
  useTemplate(templateId) {
@@ -2377,11 +1807,7 @@ var DashboardDesigner = class {
2377
1807
  const categoryId = input.categoryId ?? input.id;
2378
1808
  this.pages.push({
2379
1809
  pageId: input.id,
2380
- category: {
2381
- id: categoryId,
2382
- label: input.label ?? input.title,
2383
- scope
2384
- },
1810
+ category: { id: categoryId, label: input.label ?? input.title, scope },
2385
1811
  section: {
2386
1812
  id: input.id,
2387
1813
  title: input.title,
@@ -2395,10 +1821,6 @@ var DashboardDesigner = class {
2395
1821
  });
2396
1822
  return this;
2397
1823
  }
2398
- onHomeAction(actionId, handler) {
2399
- this.homeActions[actionId] = handler;
2400
- return this;
2401
- }
2402
1824
  onLoad(pageId, handler) {
2403
1825
  this.loadHandlers[pageId] = handler;
2404
1826
  return this;
@@ -2413,6 +1835,17 @@ var DashboardDesigner = class {
2413
1835
  onsave(pageId, handler) {
2414
1836
  return this.onSave(pageId, handler);
2415
1837
  }
1838
+ onHomeAction(actionId, handler) {
1839
+ this.homeActions[actionId] = handler;
1840
+ return this;
1841
+ }
1842
+ customCss(cssString) {
1843
+ this.partialOptions.setupDesign = {
1844
+ ...this.partialOptions.setupDesign ?? {},
1845
+ customCss: cssString
1846
+ };
1847
+ return this;
1848
+ }
2416
1849
  build() {
2417
1850
  const staticCategories = this.categories.map((item) => item.buildCategory());
2418
1851
  const staticSections = this.categories.flatMap((item) => item.buildSections());
@@ -2421,20 +1854,14 @@ var DashboardDesigner = class {
2421
1854
  const categoryMap = /* @__PURE__ */ new Map();
2422
1855
  for (const category of [...staticCategories, ...pageCategories]) {
2423
1856
  const key = `${category.scope}:${category.id}`;
2424
- if (!categoryMap.has(key)) {
2425
- categoryMap.set(key, category);
2426
- }
1857
+ if (!categoryMap.has(key)) categoryMap.set(key, category);
2427
1858
  }
2428
1859
  const categories = [...categoryMap.values()];
2429
1860
  const saveActionIds = {};
2430
1861
  for (const section of baseSections) {
2431
- if (this.saveHandlers[section.id]) {
2432
- saveActionIds[section.id] = `save:${section.id}`;
2433
- }
1862
+ if (this.saveHandlers[section.id]) saveActionIds[section.id] = `save:${section.id}`;
2434
1863
  }
2435
- const resolvedActions = {
2436
- ...this.homeActions
2437
- };
1864
+ const resolvedActions = { ...this.homeActions };
2438
1865
  for (const [sectionId, handler] of Object.entries(this.saveHandlers)) {
2439
1866
  resolvedActions[saveActionIds[sectionId]] = handler;
2440
1867
  }
@@ -2450,11 +1877,7 @@ var DashboardDesigner = class {
2450
1877
  };
2451
1878
  const saveActionId = saveActionIds[section.id];
2452
1879
  if (saveActionId && !section.actions?.some((action) => action.id === saveActionId)) {
2453
- section.actions = [...section.actions ?? [], {
2454
- id: saveActionId,
2455
- label: "Save",
2456
- variant: "primary"
2457
- }];
1880
+ section.actions = [...section.actions ?? [], { id: saveActionId, label: "Save", variant: "primary" }];
2458
1881
  }
2459
1882
  const loadHandler = this.loadHandlers[section.id];
2460
1883
  if (loadHandler) {
@@ -2479,17 +1902,452 @@ var DashboardDesigner = class {
2479
1902
  home
2480
1903
  };
2481
1904
  }
1905
+ /**
1906
+ * Builds the configuration and immediately instantiates the Dashboard.
1907
+ */
1908
+ createDashboard() {
1909
+ const options = this.build();
1910
+ return new DiscordDashboard(options);
1911
+ }
2482
1912
  };
2483
- function createDashboardDesigner(baseOptions) {
2484
- return new DashboardDesigner(baseOptions);
1913
+
1914
+ // src/index.ts
1915
+ var DISCORD_API = "https://discord.com/api/v10";
1916
+ var MANAGE_GUILD_PERMISSION = 0x20n;
1917
+ var ADMIN_PERMISSION = 0x8n;
1918
+ function normalizeBasePath(basePath) {
1919
+ if (!basePath || basePath === "/") return "/dashboard";
1920
+ return basePath.startsWith("/") ? basePath : `/${basePath}`;
1921
+ }
1922
+ function canManageGuild(permissions) {
1923
+ const value = BigInt(permissions);
1924
+ return (value & MANAGE_GUILD_PERMISSION) === MANAGE_GUILD_PERMISSION || (value & ADMIN_PERMISSION) === ADMIN_PERMISSION;
1925
+ }
1926
+ function toQuery(params) {
1927
+ const url = new URLSearchParams();
1928
+ for (const [key, value] of Object.entries(params)) {
1929
+ url.set(key, value);
1930
+ }
1931
+ return url.toString();
1932
+ }
1933
+ async function fetchDiscord(path, token) {
1934
+ const response = await fetch(`${DISCORD_API}${path}`, {
1935
+ headers: { Authorization: `Bearer ${token}` }
1936
+ });
1937
+ if (!response.ok) throw new Error(`Discord API request failed (${response.status})`);
1938
+ return await response.json();
2485
1939
  }
1940
+ function getUserAvatarUrl(user) {
1941
+ if (user.avatar) {
1942
+ const ext = user.avatar.startsWith("a_") ? "gif" : "png";
1943
+ return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${ext}?size=256`;
1944
+ }
1945
+ const fallbackIndex = Number((BigInt(user.id) >> 22n) % 6n);
1946
+ return `https://cdn.discordapp.com/embed/avatars/${fallbackIndex}.png`;
1947
+ }
1948
+ function getGuildIconUrl(guild) {
1949
+ if (!guild.icon) return null;
1950
+ const ext = guild.icon.startsWith("a_") ? "gif" : "png";
1951
+ return `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.${ext}?size=128`;
1952
+ }
1953
+ var DiscordDashboard = class {
1954
+ app;
1955
+ options;
1956
+ helpers;
1957
+ router;
1958
+ server;
1959
+ basePath;
1960
+ templateRenderer;
1961
+ plugins;
1962
+ constructor(options) {
1963
+ this.validateOptions(options);
1964
+ this.options = options;
1965
+ this.app = options.app ?? (0, import_express.default)();
1966
+ this.router = import_express.default.Router();
1967
+ this.helpers = new DiscordHelpers(options.botToken);
1968
+ this.basePath = normalizeBasePath(options.basePath);
1969
+ this.templateRenderer = this.resolveTemplateRenderer();
1970
+ this.plugins = options.plugins ?? [];
1971
+ if (!options.app && options.trustProxy !== void 0) {
1972
+ this.app.set("trust proxy", options.trustProxy);
1973
+ }
1974
+ this.setupMiddleware();
1975
+ this.setupRoutes();
1976
+ this.app.use(this.basePath, this.router);
1977
+ }
1978
+ validateOptions(options) {
1979
+ if (!options.botToken) throw new Error("botToken is required");
1980
+ if (!options.clientId) throw new Error("clientId is required");
1981
+ if (!options.clientSecret) throw new Error("clientSecret is required");
1982
+ if (!options.redirectUri) throw new Error("redirectUri is required");
1983
+ if (!options.sessionSecret) throw new Error("sessionSecret is required");
1984
+ }
1985
+ setupMiddleware() {
1986
+ this.router.use((0, import_compression.default)());
1987
+ this.router.use((0, import_helmet.default)({ contentSecurityPolicy: false }));
1988
+ this.router.use(import_express.default.json());
1989
+ this.router.use(
1990
+ (0, import_express_session.default)({
1991
+ name: this.options.sessionName ?? "discord_dashboard.sid",
1992
+ secret: this.options.sessionSecret,
1993
+ resave: false,
1994
+ saveUninitialized: false,
1995
+ cookie: {
1996
+ httpOnly: true,
1997
+ sameSite: "lax",
1998
+ maxAge: this.options.sessionMaxAgeMs ?? 1e3 * 60 * 60 * 24 * 7
1999
+ }
2000
+ })
2001
+ );
2002
+ }
2003
+ setupRoutes() {
2004
+ this.router.get("/", this.handleRoot.bind(this));
2005
+ this.router.get("/login", this.handleLogin.bind(this));
2006
+ this.router.get("/callback", this.handleCallback.bind(this));
2007
+ this.router.post("/logout", this.handleLogout.bind(this));
2008
+ this.router.get("/api/session", this.handleSession.bind(this));
2009
+ this.router.get("/api/guilds", this.ensureAuthenticated, this.handleGuilds.bind(this));
2010
+ this.router.get("/api/overview", this.ensureAuthenticated, this.handleOverview.bind(this));
2011
+ this.router.get("/api/home/categories", this.ensureAuthenticated, this.handleHomeCategories.bind(this));
2012
+ this.router.get("/api/home", this.ensureAuthenticated, this.handleHome.bind(this));
2013
+ this.router.get("/api/lookup/roles", this.ensureAuthenticated, this.handleLookupRoles.bind(this));
2014
+ this.router.get("/api/lookup/channels", this.ensureAuthenticated, this.handleLookupChannels.bind(this));
2015
+ this.router.get("/api/lookup/members", this.ensureAuthenticated, this.handleLookupMembers.bind(this));
2016
+ this.router.post("/api/home/:actionId", this.ensureAuthenticated, this.handleHomeAction.bind(this));
2017
+ this.router.get("/api/plugins", this.ensureAuthenticated, this.handlePlugins.bind(this));
2018
+ this.router.post("/api/plugins/:pluginId/:actionId", this.ensureAuthenticated, this.handlePluginAction.bind(this));
2019
+ }
2020
+ // --- Start/Stop Methods ---
2021
+ async start() {
2022
+ if (this.options.app || this.server) return;
2023
+ const port = this.options.port ?? 3e3;
2024
+ const host = this.options.host ?? "0.0.0.0";
2025
+ this.server = (0, import_node_http.createServer)(this.app);
2026
+ return new Promise((resolve) => {
2027
+ this.server.listen(port, host, () => resolve());
2028
+ });
2029
+ }
2030
+ async stop() {
2031
+ if (!this.server) return;
2032
+ return new Promise((resolve, reject) => {
2033
+ this.server.close((error) => {
2034
+ if (error) return reject(error);
2035
+ resolve();
2036
+ });
2037
+ this.server = void 0;
2038
+ });
2039
+ }
2040
+ // --- Route Handlers ---
2041
+ handleRoot(req, res) {
2042
+ if (!req.session.discordAuth) {
2043
+ res.redirect(`${this.basePath}/login`);
2044
+ return;
2045
+ }
2046
+ res.setHeader("Cache-Control", "no-store");
2047
+ res.type("html").send(
2048
+ this.templateRenderer({
2049
+ dashboardName: this.options.dashboardName ?? "Discord Dashboard",
2050
+ basePath: this.basePath,
2051
+ setupDesign: this.options.setupDesign
2052
+ })
2053
+ );
2054
+ }
2055
+ handleLogin(req, res) {
2056
+ const state = (0, import_node_crypto.randomBytes)(16).toString("hex");
2057
+ req.session.oauthState = state;
2058
+ const scope = (this.options.scopes && this.options.scopes.length > 0 ? this.options.scopes : ["identify", "guilds"]).join(" ");
2059
+ const query = toQuery({
2060
+ client_id: this.options.clientId,
2061
+ redirect_uri: this.options.redirectUri,
2062
+ response_type: "code",
2063
+ scope,
2064
+ state,
2065
+ prompt: "none"
2066
+ });
2067
+ res.redirect(`https://discord.com/oauth2/authorize?${query}`);
2068
+ }
2069
+ async handleCallback(req, res) {
2070
+ try {
2071
+ const code = typeof req.query.code === "string" ? req.query.code : void 0;
2072
+ const state = typeof req.query.state === "string" ? req.query.state : void 0;
2073
+ if (!code || !state) return res.status(400).send("Missing OAuth2 code/state");
2074
+ if (!req.session.oauthState || req.session.oauthState !== state) return res.status(403).send("Invalid OAuth2 state");
2075
+ const tokenData = await this.exchangeCodeForToken(code);
2076
+ const [user, guilds] = await Promise.all([fetchDiscord("/users/@me", tokenData.access_token), fetchDiscord("/users/@me/guilds", tokenData.access_token)]);
2077
+ req.session.discordAuth = {
2078
+ accessToken: tokenData.access_token,
2079
+ refreshToken: tokenData.refresh_token,
2080
+ expiresAt: tokenData.expires_in ? Date.now() + tokenData.expires_in * 1e3 : void 0,
2081
+ user,
2082
+ guilds
2083
+ };
2084
+ req.session.oauthState = void 0;
2085
+ res.redirect(this.basePath);
2086
+ } catch (error) {
2087
+ const message = error instanceof Error ? error.message : "OAuth callback failed";
2088
+ res.status(500).send(message);
2089
+ }
2090
+ }
2091
+ handleLogout(req, res) {
2092
+ req.session.destroy((sessionError) => {
2093
+ if (sessionError) return res.status(500).json({ ok: false, message: "Failed to destroy session" });
2094
+ res.clearCookie(this.options.sessionName ?? "discord_dashboard.sid");
2095
+ res.json({ ok: true });
2096
+ });
2097
+ }
2098
+ handleSession(req, res) {
2099
+ const auth = req.session.discordAuth;
2100
+ if (!auth) return res.status(200).json({ authenticated: false });
2101
+ const manageableGuildCount = auth.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions)).length;
2102
+ res.json({
2103
+ authenticated: true,
2104
+ user: { ...auth.user, avatarUrl: getUserAvatarUrl(auth.user) },
2105
+ guildCount: manageableGuildCount,
2106
+ expiresAt: auth.expiresAt
2107
+ });
2108
+ }
2109
+ async handleGuilds(req, res) {
2110
+ const context = this.createContext(req);
2111
+ if (this.options.ownerIds && this.options.ownerIds.length > 0 && !this.options.ownerIds.includes(context.user.id)) {
2112
+ return res.status(403).json({ message: "You are not allowed to access this dashboard." });
2113
+ }
2114
+ let manageableGuilds = context.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions));
2115
+ if (this.options.guildFilter) {
2116
+ const filtered = [];
2117
+ for (const guild of manageableGuilds) {
2118
+ if (await this.options.guildFilter(guild, context)) filtered.push(guild);
2119
+ }
2120
+ manageableGuilds = filtered;
2121
+ }
2122
+ const botGuildIds = await this.fetchBotGuildIds();
2123
+ const enrichedGuilds = manageableGuilds.map((guild) => {
2124
+ const botInGuild = botGuildIds.has(guild.id);
2125
+ return {
2126
+ ...guild,
2127
+ iconUrl: getGuildIconUrl(guild),
2128
+ botInGuild,
2129
+ inviteUrl: botInGuild ? void 0 : this.createGuildInviteUrl(guild.id)
2130
+ };
2131
+ });
2132
+ res.json({ guilds: enrichedGuilds });
2133
+ }
2134
+ async handleOverview(req, res) {
2135
+ const context = this.createContext(req);
2136
+ const cards = await this.resolveOverviewCards(context);
2137
+ res.json({ cards });
2138
+ }
2139
+ async handleHomeCategories(req, res) {
2140
+ const context = this.createContext(req);
2141
+ const activeScope = this.resolveScope(context);
2142
+ const categories = await this.resolveHomeCategories(context);
2143
+ const visible = categories.filter((item) => item.scope === "setup" || item.scope === activeScope);
2144
+ res.json({ categories: visible, activeScope });
2145
+ }
2146
+ async handleHome(req, res) {
2147
+ const context = this.createContext(req);
2148
+ const activeScope = this.resolveScope(context);
2149
+ const categoryId = typeof req.query.categoryId === "string" ? req.query.categoryId : void 0;
2150
+ let sections = await this.resolveHomeSections(context);
2151
+ sections = sections.filter((section) => {
2152
+ const sectionScope = section.scope ?? activeScope;
2153
+ if (sectionScope !== "setup" && sectionScope !== activeScope) return false;
2154
+ if (!categoryId) return true;
2155
+ return section.categoryId === categoryId;
2156
+ });
2157
+ res.json({ sections, activeScope });
2158
+ }
2159
+ async handleLookupRoles(req, res) {
2160
+ const context = this.createContext(req);
2161
+ const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
2162
+ if (!guildId) return res.status(400).json({ message: "guildId is required" });
2163
+ const query = typeof req.query.q === "string" ? req.query.q : "";
2164
+ const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
2165
+ const includeManaged = typeof req.query.includeManaged === "string" ? req.query.includeManaged === "true" : void 0;
2166
+ const roles = await context.helpers.searchGuildRoles(guildId, query, { limit: Number.isFinite(limit) ? limit : void 0, includeManaged });
2167
+ res.json({ roles });
2168
+ }
2169
+ async handleLookupChannels(req, res) {
2170
+ const context = this.createContext(req);
2171
+ const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
2172
+ if (!guildId) return res.status(400).json({ message: "guildId is required" });
2173
+ const query = typeof req.query.q === "string" ? req.query.q : "";
2174
+ const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
2175
+ const nsfw = typeof req.query.nsfw === "string" ? req.query.nsfw === "true" : void 0;
2176
+ const channelTypes = typeof req.query.channelTypes === "string" ? req.query.channelTypes.split(",").map((item) => Number(item.trim())).filter((item) => Number.isFinite(item)) : void 0;
2177
+ const channels = await context.helpers.searchGuildChannels(guildId, query, { limit: Number.isFinite(limit) ? limit : void 0, nsfw, channelTypes });
2178
+ res.json({ channels });
2179
+ }
2180
+ async handleLookupMembers(req, res) {
2181
+ const context = this.createContext(req);
2182
+ const guildId = typeof req.query.guildId === "string" && req.query.guildId.length > 0 ? req.query.guildId : context.selectedGuildId;
2183
+ if (!guildId) return res.status(400).json({ message: "guildId is required" });
2184
+ const query = typeof req.query.q === "string" ? req.query.q : "";
2185
+ const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : void 0;
2186
+ const members = await context.helpers.searchGuildMembers(guildId, query, { limit: Number.isFinite(limit) ? limit : void 0 });
2187
+ res.json({ members });
2188
+ }
2189
+ async handleHomeAction(req, res) {
2190
+ const context = this.createContext(req);
2191
+ const action = this.options.home?.actions?.[req.params.actionId];
2192
+ if (!action) return res.status(404).json({ ok: false, message: "Home action not found" });
2193
+ const payload = req.body;
2194
+ if (!payload || typeof payload.sectionId !== "string" || !payload.values || typeof payload.values !== "object") {
2195
+ return res.status(400).json({ ok: false, message: "Invalid home action payload" });
2196
+ }
2197
+ try {
2198
+ const result = await action(context, { sectionId: payload.sectionId, values: payload.values });
2199
+ res.json(result);
2200
+ } catch (error) {
2201
+ res.status(500).json({ ok: false, message: error instanceof Error ? error.message : "Home action failed" });
2202
+ }
2203
+ }
2204
+ async handlePlugins(req, res) {
2205
+ const context = this.createContext(req);
2206
+ const activeScope = context.selectedGuildId ? "guild" : "user";
2207
+ const payload = [];
2208
+ for (const plugin of this.plugins) {
2209
+ const pluginScope = plugin.scope ?? "both";
2210
+ if (pluginScope !== "both" && pluginScope !== activeScope) continue;
2211
+ const panels = await plugin.getPanels(context);
2212
+ payload.push({ id: plugin.id, name: plugin.name, description: plugin.description, panels });
2213
+ }
2214
+ res.json({ plugins: payload });
2215
+ }
2216
+ async handlePluginAction(req, res) {
2217
+ const context = this.createContext(req);
2218
+ const plugin = this.plugins.find((item) => item.id === req.params.pluginId);
2219
+ if (!plugin) return res.status(404).json({ ok: false, message: "Plugin not found" });
2220
+ const action = plugin.actions?.[req.params.actionId];
2221
+ if (!action) return res.status(404).json({ ok: false, message: "Action not found" });
2222
+ try {
2223
+ const result = await action(context, req.body);
2224
+ res.json(result);
2225
+ } catch (error) {
2226
+ res.status(500).json({ ok: false, message: error instanceof Error ? error.message : "Plugin action failed" });
2227
+ }
2228
+ }
2229
+ // --- Internal Dashboard Resolvers & Helpers ---
2230
+ ensureAuthenticated = (req, res, next) => {
2231
+ if (!req.session.discordAuth) {
2232
+ res.status(401).json({ authenticated: false, message: "Authentication required" });
2233
+ return;
2234
+ }
2235
+ next();
2236
+ };
2237
+ createContext(req) {
2238
+ const auth = req.session.discordAuth;
2239
+ if (!auth) throw new Error("Not authenticated");
2240
+ return {
2241
+ user: auth.user,
2242
+ guilds: auth.guilds,
2243
+ accessToken: auth.accessToken,
2244
+ selectedGuildId: typeof req.query.guildId === "string" ? req.query.guildId : void 0,
2245
+ helpers: this.helpers
2246
+ };
2247
+ }
2248
+ async exchangeCodeForToken(code) {
2249
+ const response = await fetch(`${DISCORD_API}/oauth2/token`, {
2250
+ method: "POST",
2251
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2252
+ body: toQuery({
2253
+ client_id: this.options.clientId,
2254
+ client_secret: this.options.clientSecret,
2255
+ grant_type: "authorization_code",
2256
+ code,
2257
+ redirect_uri: this.options.redirectUri
2258
+ })
2259
+ });
2260
+ if (!response.ok) throw new Error(`Failed token exchange: ${response.status} ${await response.text()}`);
2261
+ return await response.json();
2262
+ }
2263
+ async resolveOverviewCards(context) {
2264
+ if (this.options.getOverviewCards) return await this.options.getOverviewCards(context);
2265
+ const manageableGuildCount = context.guilds.filter((guild) => guild.owner || canManageGuild(guild.permissions)).length;
2266
+ return [
2267
+ { id: "user", title: "Logged-in User", value: context.user.global_name || context.user.username, subtitle: `ID: ${context.user.id}`, intent: "info" },
2268
+ { id: "guilds", title: "Manageable Guilds", value: manageableGuildCount, subtitle: "Owner or Manage Server permissions", intent: "success" },
2269
+ { id: "plugins", title: "Plugins Loaded", value: this.plugins.length, subtitle: "Dynamic server modules", intent: "neutral" }
2270
+ ];
2271
+ }
2272
+ async resolveHomeSections(context) {
2273
+ const customSections = this.options.home?.getSections ? await this.options.home.getSections(context) : [];
2274
+ const overviewSections = this.options.home?.getOverviewSections ? await this.options.home.getOverviewSections(context) : [];
2275
+ if (customSections.length > 0 || overviewSections.length > 0) {
2276
+ const normalizedOverview = overviewSections.map((section) => ({ ...section, categoryId: section.categoryId ?? "overview" }));
2277
+ return [...normalizedOverview, ...customSections];
2278
+ }
2279
+ const selectedGuild = context.selectedGuildId ? context.guilds.find((guild) => guild.id === context.selectedGuildId) : void 0;
2280
+ return [
2281
+ {
2282
+ id: "setup",
2283
+ title: "Setup Details",
2284
+ description: "Core dashboard setup information",
2285
+ scope: "setup",
2286
+ categoryId: "setup",
2287
+ fields: [
2288
+ { id: "dashboardName", label: "Dashboard Name", type: "text", value: this.options.dashboardName ?? "Discord Dashboard", readOnly: true },
2289
+ { id: "basePath", label: "Base Path", type: "text", value: this.basePath, readOnly: true }
2290
+ ]
2291
+ },
2292
+ {
2293
+ id: "context",
2294
+ title: "Dashboard Context",
2295
+ description: selectedGuild ? `Managing ${selectedGuild.name}` : "Managing user dashboard",
2296
+ scope: this.resolveScope(context),
2297
+ categoryId: "overview",
2298
+ fields: [
2299
+ { id: "mode", label: "Mode", type: "text", value: selectedGuild ? "Guild" : "User", readOnly: true },
2300
+ { id: "target", label: "Target", type: "text", value: selectedGuild ? selectedGuild.name : context.user.username, readOnly: true }
2301
+ ]
2302
+ }
2303
+ ];
2304
+ }
2305
+ resolveScope(context) {
2306
+ return context.selectedGuildId ? "guild" : "user";
2307
+ }
2308
+ async resolveHomeCategories(context) {
2309
+ if (this.options.home?.getCategories) {
2310
+ const categories = await this.options.home.getCategories(context);
2311
+ return [...categories].sort((a, b) => a.id === "overview" ? -1 : b.id === "overview" ? 1 : 0);
2312
+ }
2313
+ return [
2314
+ { id: "overview", label: "Overview", scope: this.resolveScope(context) },
2315
+ { id: "setup", label: "Setup", scope: "setup" }
2316
+ ];
2317
+ }
2318
+ createGuildInviteUrl(guildId) {
2319
+ const scopes = this.options.botInviteScopes && this.options.botInviteScopes.length > 0 ? this.options.botInviteScopes : ["bot", "applications.commands"];
2320
+ return `https://discord.com/oauth2/authorize?${toQuery({
2321
+ client_id: this.options.clientId,
2322
+ scope: scopes.join(" "),
2323
+ permissions: this.options.botInvitePermissions ?? "8",
2324
+ guild_id: guildId,
2325
+ disable_guild_select: "true"
2326
+ })}`;
2327
+ }
2328
+ async fetchBotGuildIds() {
2329
+ const response = await fetch(`${DISCORD_API}/users/@me/guilds`, { headers: { Authorization: `Bot ${this.options.botToken}` } });
2330
+ if (!response.ok) return /* @__PURE__ */ new Set();
2331
+ const guilds = await response.json();
2332
+ return new Set(guilds.map((guild) => guild.id));
2333
+ }
2334
+ resolveTemplateRenderer() {
2335
+ const selectedTemplate = this.options.uiTemplate ?? "default";
2336
+ const defaultRenderer = ({ dashboardName, basePath, setupDesign }) => renderDashboardHtml(dashboardName, basePath, setupDesign);
2337
+ const customRenderer = this.options.uiTemplates?.[selectedTemplate];
2338
+ if (customRenderer) return customRenderer;
2339
+ const builtinRenderer = getBuiltinTemplateRenderer(selectedTemplate);
2340
+ if (builtinRenderer) return builtinRenderer;
2341
+ if (selectedTemplate !== "default") throw new Error(`Unknown uiTemplate '${selectedTemplate}'. Register it in uiTemplates.`);
2342
+ return defaultRenderer;
2343
+ }
2344
+ };
2486
2345
  // Annotate the CommonJS export names for ESM import in node:
2487
2346
  0 && (module.exports = {
2488
2347
  DashboardDesigner,
2348
+ DiscordDashboard,
2349
+ DiscordHelpers,
2489
2350
  builtinTemplateRenderers,
2490
- createDashboard,
2491
- createDashboardDesigner,
2492
- createDiscordHelpers,
2493
2351
  getBuiltinTemplateRenderer
2494
2352
  });
2495
2353
  //# sourceMappingURL=index.cjs.map