@malto/sdk 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.js CHANGED
@@ -177,7 +177,7 @@ function buildCss(opts) {
177
177
  const primary = opts.primary;
178
178
  const tones = derivePalette(primary);
179
179
  const radiusScale = opts.radius === "sm" ? "12px" : opts.radius === "lg" ? "24px" : "20px";
180
- const appearance = opts.appearance ?? "auto";
180
+ const appearance = opts.appearance ?? "light";
181
181
  const overrides = opts.cssVars ?? {};
182
182
  return `
183
183
  @import url('https://fonts.googleapis.com/css2?family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700&display=swap');
@@ -300,7 +300,8 @@ function buildCss(opts) {
300
300
  .malto-modal {
301
301
  background: var(--malto-surface);
302
302
  width: 100%;
303
- max-width: 460px;
303
+ max-width: 1152px;
304
+ height: 720px;
304
305
  max-height: 92vh;
305
306
  border-radius: var(--malto-radius) var(--malto-radius) 0 0;
306
307
  box-shadow: var(--malto-shadow-lg);
@@ -751,6 +752,206 @@ function buildCss(opts) {
751
752
  background: var(--malto-surface-muted);
752
753
  }
753
754
 
755
+ /* \u2500\u2500\u2500\u2500\u2500\u2500 Kanban (roadmap) \u2500\u2500\u2500\u2500\u2500\u2500 */
756
+ .malto-kanban {
757
+ display: grid;
758
+ grid-template-columns: repeat(4, minmax(0, 1fr));
759
+ gap: 16px;
760
+ align-items: start;
761
+ }
762
+ @media (max-width: 960px) {
763
+ .malto-kanban { grid-template-columns: repeat(2, minmax(0, 1fr)); }
764
+ }
765
+ @media (max-width: 560px) {
766
+ .malto-kanban { grid-template-columns: 1fr; }
767
+ }
768
+ .malto-kanban-col {
769
+ display: flex; flex-direction: column;
770
+ gap: 12px;
771
+ min-width: 0;
772
+ }
773
+ .malto-kanban-head {
774
+ display: flex; align-items: center; justify-content: space-between;
775
+ padding: 0 4px;
776
+ }
777
+ .malto-kanban-title {
778
+ margin: 0;
779
+ font-size: 11px;
780
+ font-weight: 600;
781
+ text-transform: uppercase;
782
+ letter-spacing: 0.06em;
783
+ color: var(--malto-text-muted);
784
+ }
785
+ .malto-kanban-count {
786
+ font-size: 11px;
787
+ font-weight: 700;
788
+ color: var(--malto-text-subtle);
789
+ letter-spacing: 0.04em;
790
+ }
791
+ .malto-kanban-list {
792
+ display: flex; flex-direction: column;
793
+ gap: 12px;
794
+ }
795
+ .malto-kanban-empty {
796
+ padding: 14px 12px;
797
+ font-size: 12px;
798
+ color: var(--malto-text-subtle);
799
+ border: 1px dashed var(--malto-border);
800
+ border-radius: 12px;
801
+ text-align: center;
802
+ background: var(--malto-surface-muted);
803
+ }
804
+
805
+ /* Card \u2014 mirrors malto-web FeedbackCard */
806
+ .malto-card {
807
+ display: block;
808
+ text-decoration: none;
809
+ color: inherit;
810
+ position: relative;
811
+ padding: 16px;
812
+ background: var(--malto-surface);
813
+ border: 1px solid rgba(200, 196, 216, 0.18);
814
+ border-radius: 12px;
815
+ box-shadow: 0 0 40px rgba(var(--malto-primary-rgb), 0.06),
816
+ 0 0 80px rgba(var(--malto-primary-rgb), 0.02);
817
+ transition: transform .2s var(--malto-ease-spring),
818
+ box-shadow .2s ease, border-color .2s ease;
819
+ cursor: pointer;
820
+ }
821
+ .malto-card:hover {
822
+ transform: translateY(-2px);
823
+ box-shadow: 0 0 50px rgba(var(--malto-primary-rgb), 0.14),
824
+ 0 0 90px rgba(var(--malto-primary-rgb), 0.04);
825
+ border-color: rgba(var(--malto-primary-rgb), 0.2);
826
+ }
827
+ .malto-card-top {
828
+ display: flex; align-items: flex-start; justify-content: space-between;
829
+ gap: 8px;
830
+ }
831
+ .malto-card-cat {
832
+ display: inline-flex; align-items: center;
833
+ padding: 3px 9px;
834
+ border-radius: 999px;
835
+ background: rgba(var(--malto-primary-rgb), 0.1);
836
+ color: var(--malto-primary-600);
837
+ font-size: 10px;
838
+ font-weight: 600;
839
+ text-transform: uppercase;
840
+ letter-spacing: 0.05em;
841
+ line-height: 1.4;
842
+ }
843
+ .malto-card-votes {
844
+ display: inline-flex; flex-direction: column;
845
+ align-items: center;
846
+ padding: 4px 8px;
847
+ border: 1px solid var(--malto-border);
848
+ border-radius: 8px;
849
+ background: var(--malto-surface);
850
+ color: var(--malto-text);
851
+ font-size: 12px;
852
+ font-weight: 700;
853
+ line-height: 1;
854
+ transition: border-color .2s ease, background .2s ease;
855
+ }
856
+ .malto-card:hover .malto-card-votes {
857
+ border-color: rgba(var(--malto-primary-rgb), 0.3);
858
+ background: rgba(var(--malto-primary-rgb), 0.05);
859
+ }
860
+ .malto-card-votes svg {
861
+ color: var(--malto-text-muted);
862
+ margin-bottom: 2px;
863
+ }
864
+ .malto-card-title {
865
+ margin: 12px 0 0;
866
+ font-size: 14.5px;
867
+ font-weight: 600;
868
+ letter-spacing: -0.01em;
869
+ line-height: 1.35;
870
+ color: var(--malto-text);
871
+ display: -webkit-box;
872
+ -webkit-line-clamp: 2;
873
+ -webkit-box-orient: vertical;
874
+ overflow: hidden;
875
+ }
876
+ .malto-card-desc {
877
+ margin: 6px 0 0;
878
+ font-size: 12.5px;
879
+ line-height: 1.45;
880
+ color: var(--malto-text-muted);
881
+ display: -webkit-box;
882
+ -webkit-line-clamp: 2;
883
+ -webkit-box-orient: vertical;
884
+ overflow: hidden;
885
+ }
886
+ .malto-card-foot {
887
+ display: flex; align-items: center; justify-content: space-between;
888
+ gap: 8px;
889
+ margin-top: 14px;
890
+ padding-top: 12px;
891
+ border-top: 1px solid rgba(var(--malto-primary-rgb), 0.08);
892
+ font-size: 11px;
893
+ color: var(--malto-text-subtle);
894
+ }
895
+ .malto-card-who {
896
+ display: flex; align-items: center; gap: 6px;
897
+ min-width: 0;
898
+ flex: 1;
899
+ }
900
+ .malto-card-avatar {
901
+ flex: none;
902
+ width: 20px; height: 20px;
903
+ border-radius: 999px;
904
+ display: flex; align-items: center; justify-content: center;
905
+ color: #ffffff;
906
+ font-size: 9px;
907
+ font-weight: 700;
908
+ overflow: hidden;
909
+ }
910
+ .malto-card-avatar img {
911
+ width: 100%; height: 100%;
912
+ object-fit: cover;
913
+ }
914
+ .malto-avatar-bg-0 { background: #5f4ff8; }
915
+ .malto-avatar-bg-1 { background: #ec4899; }
916
+ .malto-avatar-bg-2 { background: #0ea5e9; }
917
+ .malto-avatar-bg-3 { background: #10b981; }
918
+ .malto-avatar-bg-4 { background: #f59e0b; }
919
+ .malto-avatar-bg-5 { background: #7e2eaa; }
920
+ .malto-avatar-bg-6 { background: #ef4444; }
921
+ .malto-avatar-bg-7 { background: #06b6d4; }
922
+ .malto-card-author {
923
+ font-size: 11px;
924
+ font-weight: 500;
925
+ color: var(--malto-text-muted);
926
+ min-width: 0;
927
+ overflow: hidden;
928
+ text-overflow: ellipsis;
929
+ white-space: nowrap;
930
+ }
931
+ .malto-card-dot {
932
+ color: var(--malto-border-strong);
933
+ flex: none;
934
+ }
935
+ .malto-card-time {
936
+ color: var(--malto-text-muted);
937
+ flex: none;
938
+ }
939
+ .malto-card-comments {
940
+ display: inline-flex; align-items: center; gap: 4px;
941
+ color: var(--malto-text-muted);
942
+ font-weight: 500;
943
+ flex: none;
944
+ }
945
+ .malto-card-skeleton {
946
+ cursor: default;
947
+ box-shadow: none;
948
+ }
949
+ .malto-card-skeleton:hover {
950
+ transform: none;
951
+ box-shadow: none;
952
+ border-color: rgba(200, 196, 216, 0.18);
953
+ }
954
+
754
955
  /* \u2500\u2500\u2500\u2500\u2500\u2500 Animations \u2500\u2500\u2500\u2500\u2500\u2500 */
755
956
  @keyframes malto-overlay-in {
756
957
  from { opacity: 0; backdrop-filter: blur(0); }
@@ -840,6 +1041,11 @@ var MaltoWidget = class {
840
1041
  this.container = null;
841
1042
  this.trigger = null;
842
1043
  this.board = null;
1044
+ this.shellEl = null;
1045
+ this.bodyEl = null;
1046
+ this.tabRefs = {};
1047
+ this.bannerEl = null;
1048
+ this.initialLoaded = false;
843
1049
  if (!config.apiKey) throw new Error("Malto: apiKey is required");
844
1050
  this.apiKeyPrefix = config.apiKey.slice(0, 16);
845
1051
  this.config = {
@@ -852,7 +1058,7 @@ var MaltoWidget = class {
852
1058
  cssVars: config.cssVars,
853
1059
  customCss: config.customCss,
854
1060
  radius: config.radius ?? "md",
855
- appearance: config.appearance ?? "auto",
1061
+ appearance: config.appearance ?? "light",
856
1062
  zIndex: config.zIndex ?? 2147483600,
857
1063
  views: config.views,
858
1064
  identify: config.identify,
@@ -863,10 +1069,9 @@ var MaltoWidget = class {
863
1069
  const session = readSession(this.apiKeyPrefix);
864
1070
  this.state = {
865
1071
  open: false,
866
- view: "list",
867
- loading: false,
1072
+ view: "roadmap",
1073
+ loading: true,
868
1074
  error: null,
869
- feedbacks: [],
870
1075
  roadmap: {},
871
1076
  releases: [],
872
1077
  selectedFeedback: null,
@@ -899,17 +1104,28 @@ var MaltoWidget = class {
899
1104
  await this.runIdentify(this.config.identify);
900
1105
  }
901
1106
  this.renderHost(this.resolvedMode);
1107
+ this.ensureInitialLoad();
902
1108
  this.config.onReady?.();
903
1109
  } catch (err) {
904
1110
  this.config.onError?.(err);
905
1111
  }
906
1112
  }
1113
+ ensureInitialLoad() {
1114
+ if (this.initialLoaded) return;
1115
+ this.initialLoaded = true;
1116
+ if (this.state.view === "roadmap") void this.loadRoadmap();
1117
+ else if (this.state.view === "changelog") void this.loadReleases();
1118
+ }
907
1119
  unmount() {
908
1120
  this.root?.parentElement?.removeChild(this.root);
909
1121
  this.trigger?.parentElement?.removeChild(this.trigger);
910
1122
  this.root = null;
911
1123
  this.trigger = null;
912
1124
  this.container = null;
1125
+ this.shellEl = null;
1126
+ this.bodyEl = null;
1127
+ this.tabRefs = {};
1128
+ this.bannerEl = null;
913
1129
  }
914
1130
  open() {
915
1131
  this.state.open = true;
@@ -1018,24 +1234,39 @@ var MaltoWidget = class {
1018
1234
  if (!this.root) return;
1019
1235
  if (this.resolvedMode === "inline") {
1020
1236
  if (!this.container) return;
1021
- this.container.innerHTML = "";
1022
- this.container.appendChild(this.buildPanelBody());
1237
+ if (!this.shellEl || this.shellEl.parentElement !== this.container) {
1238
+ this.container.innerHTML = "";
1239
+ this.shellEl = this.buildShell();
1240
+ this.container.appendChild(this.shellEl);
1241
+ }
1242
+ this.refreshShell();
1023
1243
  return;
1024
1244
  }
1025
- this.root.innerHTML = "";
1026
- if (!this.state.open) return;
1027
- const overlay = document.createElement("div");
1028
- overlay.className = "malto-overlay";
1029
- overlay.addEventListener("click", (e) => {
1030
- if (e.target === overlay) this.close();
1031
- });
1032
- const modal = document.createElement("div");
1033
- modal.className = "malto-modal";
1034
- modal.appendChild(this.buildPanelBody());
1035
- overlay.appendChild(modal);
1036
- this.root.appendChild(overlay);
1245
+ if (!this.state.open) {
1246
+ this.root.innerHTML = "";
1247
+ this.shellEl = null;
1248
+ this.bodyEl = null;
1249
+ this.tabRefs = {};
1250
+ this.bannerEl = null;
1251
+ return;
1252
+ }
1253
+ if (!this.shellEl || this.shellEl.parentElement === null) {
1254
+ this.root.innerHTML = "";
1255
+ const overlay = document.createElement("div");
1256
+ overlay.className = "malto-overlay";
1257
+ overlay.addEventListener("click", (e) => {
1258
+ if (e.target === overlay) this.close();
1259
+ });
1260
+ const modal = document.createElement("div");
1261
+ modal.className = "malto-modal";
1262
+ this.shellEl = this.buildShell();
1263
+ modal.appendChild(this.shellEl);
1264
+ overlay.appendChild(modal);
1265
+ this.root.appendChild(overlay);
1266
+ }
1267
+ this.refreshShell();
1037
1268
  }
1038
- buildPanelBody() {
1269
+ buildShell() {
1039
1270
  const wrap = document.createElement("div");
1040
1271
  wrap.style.display = "flex";
1041
1272
  wrap.style.flexDirection = "column";
@@ -1045,23 +1276,39 @@ var MaltoWidget = class {
1045
1276
  wrap.appendChild(this.buildTabs());
1046
1277
  const body = document.createElement("div");
1047
1278
  body.className = "malto-body";
1048
- body.appendChild(this.buildView());
1279
+ this.bodyEl = body;
1049
1280
  wrap.appendChild(body);
1050
- if (this.state.view !== "auth" && !this.state.email) {
1051
- const banner = document.createElement("div");
1052
- banner.className = "malto-footer-banner";
1053
- const txt = document.createElement("span");
1054
- txt.innerHTML = `Sign in to vote, comment, or submit ideas.`;
1055
- const link = document.createElement("button");
1056
- link.className = "malto-link";
1057
- link.textContent = "Sign in";
1058
- link.addEventListener("click", () => this.go("auth"));
1059
- banner.appendChild(txt);
1060
- banner.appendChild(link);
1061
- wrap.appendChild(banner);
1062
- }
1281
+ const banner = document.createElement("div");
1282
+ banner.className = "malto-footer-banner";
1283
+ banner.style.display = "none";
1284
+ const txt = document.createElement("span");
1285
+ txt.innerHTML = `Sign in to vote, comment, or submit ideas.`;
1286
+ const link = document.createElement("button");
1287
+ link.className = "malto-link";
1288
+ link.textContent = "Sign in";
1289
+ link.addEventListener("click", () => this.go("auth"));
1290
+ banner.appendChild(txt);
1291
+ banner.appendChild(link);
1292
+ this.bannerEl = banner;
1293
+ wrap.appendChild(banner);
1063
1294
  return wrap;
1064
1295
  }
1296
+ refreshShell() {
1297
+ for (const [name, btn] of Object.entries(this.tabRefs)) {
1298
+ btn?.setAttribute(
1299
+ "aria-selected",
1300
+ name === this.state.view ? "true" : "false"
1301
+ );
1302
+ }
1303
+ if (this.bodyEl) {
1304
+ this.bodyEl.innerHTML = "";
1305
+ this.bodyEl.appendChild(this.buildView());
1306
+ }
1307
+ if (this.bannerEl) {
1308
+ const showBanner = this.state.view !== "auth" && !this.state.email;
1309
+ this.bannerEl.style.display = showBanner ? "" : "none";
1310
+ }
1311
+ }
1065
1312
  buildHeader() {
1066
1313
  const header = document.createElement("div");
1067
1314
  header.className = "malto-header";
@@ -1098,14 +1345,14 @@ var MaltoWidget = class {
1098
1345
  buildTabs() {
1099
1346
  const tabs = document.createElement("div");
1100
1347
  tabs.className = "malto-tabs";
1348
+ this.tabRefs = {};
1101
1349
  const enabled = this.allowedViews();
1102
- if (enabled.includes("list")) tabs.appendChild(this.tabBtn("Feed", "list"));
1103
- if (enabled.includes("submit"))
1104
- tabs.appendChild(this.tabBtn("New", "submit"));
1105
1350
  if (enabled.includes("roadmap"))
1106
1351
  tabs.appendChild(this.tabBtn("Roadmap", "roadmap"));
1107
1352
  if (enabled.includes("changelog"))
1108
1353
  tabs.appendChild(this.tabBtn("Updates", "changelog"));
1354
+ if (enabled.includes("submit"))
1355
+ tabs.appendChild(this.tabBtn("New", "submit"));
1109
1356
  return tabs;
1110
1357
  }
1111
1358
  tabBtn(label, view) {
@@ -1117,13 +1364,16 @@ var MaltoWidget = class {
1117
1364
  );
1118
1365
  btn.textContent = label;
1119
1366
  btn.addEventListener("click", () => this.go(view));
1367
+ this.tabRefs[view] = btn;
1120
1368
  return btn;
1121
1369
  }
1122
1370
  go(view) {
1123
1371
  this.state.view = view;
1124
1372
  this.state.error = null;
1373
+ if (view === "roadmap" || view === "changelog" || view === "detail" && this.state.selectedFeedback) {
1374
+ this.state.loading = true;
1375
+ }
1125
1376
  this.render();
1126
- if (view === "list") void this.loadFeedbacks();
1127
1377
  if (view === "roadmap") void this.loadRoadmap();
1128
1378
  if (view === "changelog") void this.loadReleases();
1129
1379
  if (view === "detail" && this.state.selectedFeedback) {
@@ -1131,21 +1381,12 @@ var MaltoWidget = class {
1131
1381
  }
1132
1382
  }
1133
1383
  allowedViews() {
1134
- const settings = this.board?.widget.enabledFeatures ?? [
1135
- "list",
1136
- "submit",
1137
- "vote",
1138
- "comment",
1139
- "roadmap",
1140
- "changelog"
1141
- ];
1142
- const requested = this.config.views ?? [
1143
- "list",
1144
- "submit",
1145
- "roadmap",
1146
- "changelog"
1147
- ];
1148
- return requested.filter((v) => settings.includes(v));
1384
+ const TABS = ["roadmap", "submit", "changelog"];
1385
+ const settings = this.board?.widget.enabledFeatures ?? TABS;
1386
+ const requested = this.config.views ?? TABS;
1387
+ return TABS.filter(
1388
+ (v) => settings.includes(v) && requested.includes(v)
1389
+ );
1149
1390
  }
1150
1391
  buildView() {
1151
1392
  const wrap = document.createElement("div");
@@ -1156,12 +1397,6 @@ var MaltoWidget = class {
1156
1397
  wrap.appendChild(e);
1157
1398
  }
1158
1399
  switch (this.state.view) {
1159
- case "list":
1160
- wrap.appendChild(this.viewList());
1161
- if (this.state.feedbacks.length === 0 && !this.state.loading) {
1162
- void this.loadFeedbacks();
1163
- }
1164
- break;
1165
1400
  case "submit":
1166
1401
  wrap.appendChild(this.viewSubmit());
1167
1402
  break;
@@ -1170,15 +1405,9 @@ var MaltoWidget = class {
1170
1405
  break;
1171
1406
  case "roadmap":
1172
1407
  wrap.appendChild(this.viewRoadmap());
1173
- if (Object.keys(this.state.roadmap).length === 0 && !this.state.loading) {
1174
- void this.loadRoadmap();
1175
- }
1176
1408
  break;
1177
1409
  case "changelog":
1178
1410
  wrap.appendChild(this.viewChangelog());
1179
- if (this.state.releases.length === 0 && !this.state.loading) {
1180
- void this.loadReleases();
1181
- }
1182
1411
  break;
1183
1412
  case "detail":
1184
1413
  wrap.appendChild(this.viewDetail());
@@ -1186,30 +1415,6 @@ var MaltoWidget = class {
1186
1415
  }
1187
1416
  return wrap;
1188
1417
  }
1189
- viewList() {
1190
- const wrap = document.createElement("div");
1191
- if (this.state.loading) {
1192
- wrap.appendChild(this.skeletonList(4));
1193
- return wrap;
1194
- }
1195
- if (this.state.feedbacks.length === 0) {
1196
- wrap.appendChild(
1197
- this.emptyState(
1198
- "\u{1F4A1}",
1199
- "No requests yet",
1200
- "Be the first to share an idea or report something."
1201
- )
1202
- );
1203
- return wrap;
1204
- }
1205
- const list = document.createElement("div");
1206
- list.className = "malto-list";
1207
- for (const fb of this.state.feedbacks) {
1208
- list.appendChild(this.feedbackRow(fb));
1209
- }
1210
- wrap.appendChild(list);
1211
- return wrap;
1212
- }
1213
1418
  feedbackRow(fb) {
1214
1419
  const row = document.createElement("div");
1215
1420
  row.className = "malto-item";
@@ -1289,8 +1494,7 @@ var MaltoWidget = class {
1289
1494
  });
1290
1495
  titleInput.value = "";
1291
1496
  descInput.value = "";
1292
- await this.loadFeedbacks();
1293
- this.go("list");
1497
+ this.go("roadmap");
1294
1498
  } catch (err) {
1295
1499
  this.state.error = err.message;
1296
1500
  submit.removeAttribute("disabled");
@@ -1314,7 +1518,7 @@ var MaltoWidget = class {
1314
1518
  clearSession(this.apiKeyPrefix);
1315
1519
  this.state.email = null;
1316
1520
  this.state.name = null;
1317
- this.go("list");
1521
+ this.go("roadmap");
1318
1522
  });
1319
1523
  wrap.appendChild(p);
1320
1524
  wrap.appendChild(out);
@@ -1349,7 +1553,7 @@ var MaltoWidget = class {
1349
1553
  this.state.email = session.user.email;
1350
1554
  this.state.name = session.user.name;
1351
1555
  this.state.authStatus = "idle";
1352
- this.go("list");
1556
+ this.go("roadmap");
1353
1557
  } catch (err) {
1354
1558
  this.state.error = err.message;
1355
1559
  verifyBtn.removeAttribute("disabled");
@@ -1410,40 +1614,57 @@ var MaltoWidget = class {
1410
1614
  }
1411
1615
  viewRoadmap() {
1412
1616
  const wrap = document.createElement("div");
1617
+ wrap.className = "malto-kanban";
1413
1618
  if (this.state.loading) {
1414
- wrap.appendChild(this.skeletonList(3));
1619
+ for (let i = 0; i < 4; i++) {
1620
+ const col = document.createElement("div");
1621
+ col.className = "malto-kanban-col";
1622
+ const h = document.createElement("h5");
1623
+ h.className = "malto-kanban-title";
1624
+ h.textContent = "Loading";
1625
+ col.appendChild(h);
1626
+ col.appendChild(this.skeletonCards(2));
1627
+ wrap.appendChild(col);
1628
+ }
1415
1629
  return wrap;
1416
1630
  }
1417
- const labels = {
1418
- PLANNED: "Planned",
1419
- IN_PROGRESS: "In Progress",
1420
- COMPLETED: "Done"
1421
- };
1631
+ const cols = [
1632
+ { status: "BACKLOG", title: "Backlog" },
1633
+ { status: "PLANNED", title: "Planned" },
1634
+ { status: "IN_PROGRESS", title: "In Progress" },
1635
+ { status: "UNDER_REVIEW", title: "Under Review" }
1636
+ ];
1422
1637
  let any = false;
1423
- for (const status of ["PLANNED", "IN_PROGRESS", "COMPLETED"]) {
1638
+ for (const { status, title } of cols) {
1424
1639
  const items = this.state.roadmap[status] ?? [];
1640
+ if (items.length > 0) any = true;
1425
1641
  const col = document.createElement("div");
1426
- col.className = "malto-roadmap-col";
1642
+ col.className = "malto-kanban-col";
1643
+ const head = document.createElement("div");
1644
+ head.className = "malto-kanban-head";
1427
1645
  const h = document.createElement("h5");
1428
- h.textContent = labels[status] ?? status;
1646
+ h.className = "malto-kanban-title";
1647
+ h.textContent = title;
1429
1648
  const c = document.createElement("span");
1430
- c.className = "malto-roadmap-count";
1431
- c.textContent = String(items.length);
1432
- h.appendChild(c);
1433
- col.appendChild(h);
1649
+ c.className = "malto-kanban-count";
1650
+ c.textContent = pad2(items.length);
1651
+ head.appendChild(h);
1652
+ head.appendChild(c);
1653
+ col.appendChild(head);
1654
+ const list = document.createElement("div");
1655
+ list.className = "malto-kanban-list";
1434
1656
  if (items.length === 0) {
1435
1657
  const empty = document.createElement("div");
1436
- empty.className = "malto-empty-desc";
1437
- empty.style.padding = "8px 0 12px";
1438
- empty.textContent = "Nothing here yet.";
1439
- col.appendChild(empty);
1658
+ empty.className = "malto-kanban-empty";
1659
+ empty.textContent = "Nothing here.";
1660
+ list.appendChild(empty);
1440
1661
  } else {
1441
- any = true;
1442
- for (const fb of items) col.appendChild(this.feedbackRow(fb));
1662
+ for (const fb of items) list.appendChild(this.feedbackCard(fb));
1443
1663
  }
1664
+ col.appendChild(list);
1444
1665
  wrap.appendChild(col);
1445
1666
  }
1446
- if (!any && Object.keys(this.state.roadmap).length === 0) {
1667
+ if (!any) {
1447
1668
  wrap.innerHTML = "";
1448
1669
  wrap.appendChild(
1449
1670
  this.emptyState(
@@ -1455,6 +1676,102 @@ var MaltoWidget = class {
1455
1676
  }
1456
1677
  return wrap;
1457
1678
  }
1679
+ feedbackCard(fb) {
1680
+ const url = fb.url ?? "";
1681
+ const card = document.createElement(url ? "a" : "div");
1682
+ card.className = "malto-card";
1683
+ if (url) {
1684
+ card.href = url;
1685
+ card.target = "_blank";
1686
+ card.rel = "noopener noreferrer";
1687
+ }
1688
+ const top = document.createElement("div");
1689
+ top.className = "malto-card-top";
1690
+ const cat = document.createElement("span");
1691
+ cat.className = "malto-card-cat";
1692
+ cat.textContent = fb.category ?? "General";
1693
+ const votes = document.createElement("span");
1694
+ votes.className = "malto-card-votes";
1695
+ votes.innerHTML = `<svg viewBox="0 0 12 12" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 7 6 4 9 7"/></svg><span>${formatCount(fb.votes ?? 0)}</span>`;
1696
+ top.appendChild(cat);
1697
+ top.appendChild(votes);
1698
+ card.appendChild(top);
1699
+ const title = document.createElement("h4");
1700
+ title.className = "malto-card-title";
1701
+ title.textContent = fb.title;
1702
+ card.appendChild(title);
1703
+ if (fb.description) {
1704
+ const desc = document.createElement("p");
1705
+ desc.className = "malto-card-desc";
1706
+ desc.textContent = fb.description;
1707
+ card.appendChild(desc);
1708
+ }
1709
+ const foot = document.createElement("div");
1710
+ foot.className = "malto-card-foot";
1711
+ const who = document.createElement("div");
1712
+ who.className = "malto-card-who";
1713
+ const avatarUrl = safeImageUrl(fb.author?.avatarUrl);
1714
+ const avatar = document.createElement("span");
1715
+ avatar.className = "malto-card-avatar";
1716
+ if (avatarUrl) {
1717
+ const img = document.createElement("img");
1718
+ img.src = avatarUrl;
1719
+ img.alt = "";
1720
+ img.referrerPolicy = "no-referrer";
1721
+ img.crossOrigin = "anonymous";
1722
+ avatar.appendChild(img);
1723
+ } else {
1724
+ avatar.classList.add(`malto-avatar-bg-${avatarBgIndex(fb.author?.name ?? fb.title)}`);
1725
+ avatar.textContent = initialsOf(fb.author?.name);
1726
+ }
1727
+ who.appendChild(avatar);
1728
+ const name = document.createElement("span");
1729
+ name.className = "malto-card-author";
1730
+ name.textContent = fb.author?.name ?? "Anon";
1731
+ who.appendChild(name);
1732
+ const dot = document.createElement("span");
1733
+ dot.className = "malto-card-dot";
1734
+ dot.textContent = "\xB7";
1735
+ who.appendChild(dot);
1736
+ const time = document.createElement("span");
1737
+ time.className = "malto-card-time";
1738
+ time.textContent = relativeTime(fb.createdAt);
1739
+ who.appendChild(time);
1740
+ const comments = document.createElement("span");
1741
+ comments.className = "malto-card-comments";
1742
+ comments.innerHTML = `<svg viewBox="0 0 14 14" width="11" height="11" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9a1.5 1.5 0 0 1-1.5 1.5H5l-3 3v-9A1.5 1.5 0 0 1 3.5 3h7A1.5 1.5 0 0 1 12 4.5z"/></svg><span>${fb.commentCount ?? 0}</span>`;
1743
+ foot.appendChild(who);
1744
+ foot.appendChild(comments);
1745
+ card.appendChild(foot);
1746
+ return card;
1747
+ }
1748
+ skeletonCards(count) {
1749
+ const wrap = document.createElement("div");
1750
+ wrap.className = "malto-kanban-list";
1751
+ for (let i = 0; i < count; i++) {
1752
+ const card = document.createElement("div");
1753
+ card.className = "malto-card malto-card-skeleton";
1754
+ const top = document.createElement("div");
1755
+ top.className = "malto-skeleton";
1756
+ top.style.height = "14px";
1757
+ top.style.width = "60px";
1758
+ const t = document.createElement("div");
1759
+ t.className = "malto-skeleton";
1760
+ t.style.height = "12px";
1761
+ t.style.width = "85%";
1762
+ t.style.marginTop = "10px";
1763
+ const d = document.createElement("div");
1764
+ d.className = "malto-skeleton";
1765
+ d.style.height = "10px";
1766
+ d.style.width = "70%";
1767
+ d.style.marginTop = "6px";
1768
+ card.appendChild(top);
1769
+ card.appendChild(t);
1770
+ card.appendChild(d);
1771
+ wrap.appendChild(card);
1772
+ }
1773
+ return wrap;
1774
+ }
1458
1775
  viewChangelog() {
1459
1776
  const wrap = document.createElement("div");
1460
1777
  if (this.state.loading) {
@@ -1491,18 +1808,20 @@ var MaltoWidget = class {
1491
1808
  const fb = this.state.selectedFeedback;
1492
1809
  if (!fb) {
1493
1810
  wrap.appendChild(
1494
- this.emptyState("\u{1F4ED}", "Nothing selected", "Pick a request from Feed.")
1811
+ this.emptyState("\u{1F4ED}", "Nothing selected", "Pick a request from the roadmap.")
1495
1812
  );
1496
1813
  return wrap;
1497
1814
  }
1498
1815
  const back = document.createElement("button");
1499
1816
  back.className = "malto-back";
1500
1817
  back.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg> Back';
1501
- back.addEventListener("click", () => this.go("list"));
1818
+ back.addEventListener("click", () => this.go("roadmap"));
1502
1819
  wrap.appendChild(back);
1503
1820
  wrap.appendChild(this.feedbackRow(fb));
1504
- const allowed = this.allowedViews();
1505
- if (allowed.includes("comment")) {
1821
+ const commentEnabled = this.board?.widget.enabledFeatures.includes(
1822
+ "comment"
1823
+ ) ?? true;
1824
+ if (commentEnabled) {
1506
1825
  const commentsHeader = document.createElement("h5");
1507
1826
  commentsHeader.textContent = "Comments";
1508
1827
  commentsHeader.style.cssText = "margin: 16px 0 8px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--malto-text-muted); font-weight: 600;";
@@ -1636,18 +1955,6 @@ var MaltoWidget = class {
1636
1955
  wrap.appendChild(d);
1637
1956
  return wrap;
1638
1957
  }
1639
- async loadFeedbacks() {
1640
- this.state.loading = true;
1641
- this.render();
1642
- try {
1643
- this.state.feedbacks = await this.client.listFeedbacks();
1644
- } catch (err) {
1645
- this.state.error = err.message;
1646
- } finally {
1647
- this.state.loading = false;
1648
- this.render();
1649
- }
1650
- }
1651
1958
  async loadRoadmap() {
1652
1959
  this.state.loading = true;
1653
1960
  this.render();
@@ -1719,6 +2026,22 @@ function statusPill(status) {
1719
2026
  pill.appendChild(txt);
1720
2027
  return pill;
1721
2028
  }
2029
+ function initialsOf(name) {
2030
+ if (!name) return "?";
2031
+ const parts = name.trim().split(/\s+/).filter(Boolean);
2032
+ if (parts.length === 0) return "?";
2033
+ const first = parts[0]?.[0] ?? "";
2034
+ const last = parts.length > 1 ? parts[parts.length - 1]?.[0] ?? "" : "";
2035
+ return (first + last).toUpperCase() || "?";
2036
+ }
2037
+ function avatarBgIndex(seedStr) {
2038
+ let h = 0;
2039
+ for (let i = 0; i < seedStr.length; i++) h = h * 31 + seedStr.charCodeAt(i) >>> 0;
2040
+ return h % 8;
2041
+ }
2042
+ function pad2(n) {
2043
+ return n < 10 ? `0${n}` : String(n);
2044
+ }
1722
2045
  function formatCount(n) {
1723
2046
  if (n < 1e3) return String(n);
1724
2047
  if (n < 1e4) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";