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