@malto/sdk 0.1.1 → 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
@@ -178,10 +178,12 @@ function upsert(id, css) {
178
178
  function buildCss(opts) {
179
179
  const primary = opts.primary;
180
180
  const tones = derivePalette(primary);
181
- const radiusScale = opts.radius === "sm" ? "10px" : opts.radius === "lg" ? "22px" : "16px";
182
- const appearance = opts.appearance ?? "auto";
181
+ const radiusScale = opts.radius === "sm" ? "12px" : opts.radius === "lg" ? "24px" : "20px";
182
+ const appearance = opts.appearance ?? "light";
183
183
  const overrides = opts.cssVars ?? {};
184
184
  return `
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');
186
+
185
187
  :where(.malto-root, .malto-root *) {
186
188
  box-sizing: border-box;
187
189
  }
@@ -194,21 +196,21 @@ function buildCss(opts) {
194
196
  --malto-primary-600: ${tones.shade600};
195
197
  --malto-primary-rgb: ${tones.rgb};
196
198
  --malto-surface: ${overrides.surface ?? "#ffffff"};
197
- --malto-surface-muted: ${overrides.surfaceMuted ?? "#fbfaff"};
198
- --malto-surface-elevated: rgba(255,255,255,0.85);
199
- --malto-border: ${overrides.border ?? "rgba(15, 13, 35, 0.06)"};
200
- --malto-border-strong: rgba(15, 13, 35, 0.1);
201
- --malto-text: ${overrides.text ?? "#0f0d23"};
199
+ --malto-surface-muted: ${overrides.surfaceMuted ?? "#f2eeff"};
200
+ --malto-surface-elevated: rgba(255,255,255,0.92);
201
+ --malto-border: ${overrides.border ?? "#e2e8f0"};
202
+ --malto-border-strong: #cbd1de;
203
+ --malto-text: ${overrides.text ?? "#0a0a0a"};
202
204
  --malto-text-muted: ${overrides.textMuted ?? "#5b5871"};
203
- --malto-text-subtle: rgba(15, 13, 35, 0.45);
205
+ --malto-text-subtle: rgba(10, 10, 10, 0.5);
204
206
  --malto-radius: ${overrides.radius ?? radiusScale};
205
- --malto-radius-sm: 10px;
207
+ --malto-radius-sm: 12px;
206
208
  --malto-radius-pill: 999px;
207
- --malto-shadow-sm: ${overrides.shadow ?? "0 4px 12px -4px rgba(15, 13, 35, 0.08)"};
208
- --malto-shadow-md: 0 12px 32px -8px rgba(15, 13, 35, 0.16);
209
- --malto-shadow-lg: 0 30px 60px -20px rgba(15, 13, 35, 0.28);
210
- --malto-shadow-glow: 0 12px 24px -8px rgba(var(--malto-primary-rgb), 0.45);
211
- --malto-font: ${overrides.font ?? "-apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', Roboto, system-ui, sans-serif"};
209
+ --malto-shadow-sm: ${overrides.shadow ?? "0 4px 12px -6px rgba(25, 24, 56, 0.12)"};
210
+ --malto-shadow-md: 0 10px 30px -10px rgba(25, 24, 56, 0.15);
211
+ --malto-shadow-lg: 0 30px 60px -20px rgba(25, 24, 56, 0.25);
212
+ --malto-shadow-glow: 0 8px 20px -6px rgba(var(--malto-primary-rgb), 0.45);
213
+ --malto-font: ${overrides.font ?? "'Google Sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif"};
212
214
  --malto-ease-spring: cubic-bezier(0.34, 1.4, 0.5, 1);
213
215
  --malto-ease-smooth: cubic-bezier(0.32, 0.72, 0, 1);
214
216
  }
@@ -269,12 +271,12 @@ function buildCss(opts) {
269
271
 
270
272
  .malto-trigger {
271
273
  display: inline-flex; align-items: center; gap: 8px;
272
- padding: 9px 18px;
274
+ padding: 10px 20px;
273
275
  border-radius: var(--malto-radius-pill);
274
276
  background: var(--malto-primary);
275
277
  color: var(--malto-primary-contrast);
276
278
  border: none; cursor: pointer;
277
- font-size: 13px; font-weight: 600; letter-spacing: -0.01em;
279
+ font-size: 14px; font-weight: 600; letter-spacing: -0.01em;
278
280
  box-shadow: var(--malto-shadow-glow);
279
281
  transition: transform .25s var(--malto-ease-spring), box-shadow .2s ease;
280
282
  }
@@ -300,7 +302,8 @@ function buildCss(opts) {
300
302
  .malto-modal {
301
303
  background: var(--malto-surface);
302
304
  width: 100%;
303
- max-width: 460px;
305
+ max-width: 1152px;
306
+ height: 720px;
304
307
  max-height: 92vh;
305
308
  border-radius: var(--malto-radius) var(--malto-radius) 0 0;
306
309
  box-shadow: var(--malto-shadow-lg);
@@ -328,23 +331,23 @@ function buildCss(opts) {
328
331
  /* \u2500\u2500\u2500\u2500\u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500 */
329
332
  .malto-header {
330
333
  display: flex; align-items: center; justify-content: space-between;
331
- padding: 18px 22px 14px;
334
+ padding: 22px 24px 16px;
332
335
  position: relative;
333
336
  flex: none;
334
337
  }
335
338
  .malto-header h3 {
336
339
  margin: 0;
337
- font-size: 16px; font-weight: 600;
340
+ font-size: 17px; font-weight: 600;
338
341
  color: var(--malto-text);
339
- letter-spacing: -0.015em;
342
+ letter-spacing: -0.02em;
340
343
  }
341
344
  .malto-header-meta {
342
345
  display: flex; align-items: center; gap: 10px;
343
346
  }
344
347
  .malto-board-logo {
345
348
  width: 28px; height: 28px;
346
- border-radius: 8px;
347
- background: var(--malto-primary-100);
349
+ border-radius: 10px;
350
+ background: var(--malto-surface-muted);
348
351
  display: flex; align-items: center; justify-content: center;
349
352
  color: var(--malto-primary);
350
353
  font-size: 12px; font-weight: 700;
@@ -362,7 +365,7 @@ function buildCss(opts) {
362
365
  transition: background .15s ease, color .15s ease;
363
366
  }
364
367
  .malto-close:hover {
365
- background: var(--malto-primary-50);
368
+ background: var(--malto-surface-muted);
366
369
  color: var(--malto-text);
367
370
  }
368
371
 
@@ -370,8 +373,8 @@ function buildCss(opts) {
370
373
  .malto-tabs {
371
374
  display: flex;
372
375
  padding: 4px;
373
- margin: 0 18px 14px;
374
- background: var(--malto-primary-50);
376
+ margin: 0 24px 16px;
377
+ background: var(--malto-surface-muted);
375
378
  border-radius: var(--malto-radius-sm);
376
379
  flex: none;
377
380
  }
@@ -397,7 +400,7 @@ function buildCss(opts) {
397
400
  .malto-body {
398
401
  flex: 1;
399
402
  overflow-y: auto;
400
- padding: 4px 18px 18px;
403
+ padding: 4px 24px 22px;
401
404
  background: var(--malto-surface);
402
405
  scroll-behavior: smooth;
403
406
  }
@@ -412,7 +415,7 @@ function buildCss(opts) {
412
415
  /* \u2500\u2500\u2500\u2500\u2500\u2500 Feedback row \u2500\u2500\u2500\u2500\u2500\u2500 */
413
416
  .malto-item {
414
417
  display: flex; gap: 12px;
415
- padding: 12px;
418
+ padding: 14px;
416
419
  border-radius: 12px;
417
420
  background: var(--malto-surface);
418
421
  border: 1px solid var(--malto-border);
@@ -423,15 +426,15 @@ function buildCss(opts) {
423
426
  .malto-item:hover {
424
427
  border-color: var(--malto-border-strong);
425
428
  transform: translateY(-1px);
426
- box-shadow: var(--malto-shadow-sm);
429
+ box-shadow: 0 6px 16px -8px rgba(25, 24, 56, 0.12);
427
430
  }
428
431
  .malto-vote {
429
432
  display: flex; flex-direction: column;
430
433
  align-items: center; justify-content: center;
431
434
  min-width: 44px; padding: 6px 4px;
432
- border-radius: 10px;
435
+ border-radius: 12px;
433
436
  border: 1px solid var(--malto-border);
434
- background: var(--malto-surface-muted);
437
+ background: var(--malto-surface);
435
438
  cursor: pointer;
436
439
  font-size: 11px; color: var(--malto-text-muted);
437
440
  transition: all .2s var(--malto-ease-spring);
@@ -518,10 +521,10 @@ function buildCss(opts) {
518
521
  }
519
522
  .malto-input, .malto-textarea {
520
523
  width: 100%;
521
- padding: 11px 14px;
522
- border: 1px solid var(--malto-border-strong);
524
+ padding: 12px 14px;
525
+ border: 1px solid var(--malto-border);
523
526
  border-radius: 12px;
524
- font-size: 13.5px;
527
+ font-size: 14px;
525
528
  color: var(--malto-text);
526
529
  background: var(--malto-surface);
527
530
  outline: none;
@@ -533,7 +536,7 @@ function buildCss(opts) {
533
536
  }
534
537
  .malto-input:focus, .malto-textarea:focus {
535
538
  border-color: var(--malto-primary);
536
- box-shadow: 0 0 0 4px rgba(var(--malto-primary-rgb), 0.1);
539
+ box-shadow: 0 0 0 4px rgba(var(--malto-primary-rgb), 0.12);
537
540
  }
538
541
  .malto-textarea {
539
542
  min-height: 110px;
@@ -541,13 +544,13 @@ function buildCss(opts) {
541
544
  line-height: 1.5;
542
545
  }
543
546
  .malto-button {
544
- padding: 11px 18px;
547
+ padding: 12px 20px;
545
548
  border-radius: 12px;
546
549
  background: var(--malto-primary);
547
550
  color: var(--malto-primary-contrast);
548
551
  border: none; cursor: pointer;
549
- font-size: 13px; font-weight: 600;
550
- letter-spacing: -0.005em;
552
+ font-size: 14px; font-weight: 600;
553
+ letter-spacing: -0.01em;
551
554
  box-shadow: var(--malto-shadow-glow);
552
555
  transition: transform .2s var(--malto-ease-spring), box-shadow .2s ease, opacity .2s ease;
553
556
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
@@ -556,13 +559,13 @@ function buildCss(opts) {
556
559
  .malto-button:active { transform: scale(0.97); }
557
560
  .malto-button[disabled] { opacity: .5; cursor: not-allowed; transform: none; }
558
561
  .malto-secondary {
559
- padding: 9px 14px;
560
- border-radius: 10px;
562
+ padding: 10px 16px;
563
+ border-radius: 12px;
561
564
  background: transparent;
562
565
  color: var(--malto-text);
563
- border: 1px solid var(--malto-border-strong);
566
+ border: 1px solid var(--malto-border);
564
567
  cursor: pointer;
565
- font-size: 13px; font-weight: 500;
568
+ font-size: 13.5px; font-weight: 500;
566
569
  transition: all .2s ease;
567
570
  }
568
571
  .malto-secondary:hover {
@@ -655,7 +658,7 @@ function buildCss(opts) {
655
658
 
656
659
  /* \u2500\u2500\u2500\u2500\u2500\u2500 Releases / changelog \u2500\u2500\u2500\u2500\u2500\u2500 */
657
660
  .malto-release {
658
- padding: 14px;
661
+ padding: 16px;
659
662
  border: 1px solid var(--malto-border);
660
663
  border-radius: 12px;
661
664
  margin-bottom: 8px;
@@ -725,10 +728,10 @@ function buildCss(opts) {
725
728
  /* \u2500\u2500\u2500\u2500\u2500\u2500 Footer banner (sign-in CTA) \u2500\u2500\u2500\u2500\u2500\u2500 */
726
729
  .malto-footer-banner {
727
730
  flex: none;
728
- padding: 12px 18px;
731
+ padding: 14px 24px;
729
732
  background: var(--malto-surface-muted);
730
733
  border-top: 1px solid var(--malto-border);
731
- font-size: 12px;
734
+ font-size: 12.5px;
732
735
  color: var(--malto-text-muted);
733
736
  display: flex; align-items: center; justify-content: space-between;
734
737
  gap: 10px;
@@ -748,7 +751,207 @@ function buildCss(opts) {
748
751
  }
749
752
  .malto-back:hover {
750
753
  color: var(--malto-text);
751
- background: var(--malto-primary-50);
754
+ background: var(--malto-surface-muted);
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);
752
955
  }
753
956
 
754
957
  /* \u2500\u2500\u2500\u2500\u2500\u2500 Animations \u2500\u2500\u2500\u2500\u2500\u2500 */
@@ -840,6 +1043,11 @@ var MaltoWidget = class {
840
1043
  this.container = null;
841
1044
  this.trigger = null;
842
1045
  this.board = null;
1046
+ this.shellEl = null;
1047
+ this.bodyEl = null;
1048
+ this.tabRefs = {};
1049
+ this.bannerEl = null;
1050
+ this.initialLoaded = false;
843
1051
  if (!config.apiKey) throw new Error("Malto: apiKey is required");
844
1052
  this.apiKeyPrefix = config.apiKey.slice(0, 16);
845
1053
  this.config = {
@@ -852,7 +1060,7 @@ var MaltoWidget = class {
852
1060
  cssVars: config.cssVars,
853
1061
  customCss: config.customCss,
854
1062
  radius: config.radius ?? "md",
855
- appearance: config.appearance ?? "auto",
1063
+ appearance: config.appearance ?? "light",
856
1064
  zIndex: config.zIndex ?? 2147483600,
857
1065
  views: config.views,
858
1066
  identify: config.identify,
@@ -863,10 +1071,9 @@ var MaltoWidget = class {
863
1071
  const session = readSession(this.apiKeyPrefix);
864
1072
  this.state = {
865
1073
  open: false,
866
- view: "list",
867
- loading: false,
1074
+ view: "roadmap",
1075
+ loading: true,
868
1076
  error: null,
869
- feedbacks: [],
870
1077
  roadmap: {},
871
1078
  releases: [],
872
1079
  selectedFeedback: null,
@@ -899,17 +1106,28 @@ var MaltoWidget = class {
899
1106
  await this.runIdentify(this.config.identify);
900
1107
  }
901
1108
  this.renderHost(this.resolvedMode);
1109
+ this.ensureInitialLoad();
902
1110
  this.config.onReady?.();
903
1111
  } catch (err) {
904
1112
  this.config.onError?.(err);
905
1113
  }
906
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
+ }
907
1121
  unmount() {
908
1122
  this.root?.parentElement?.removeChild(this.root);
909
1123
  this.trigger?.parentElement?.removeChild(this.trigger);
910
1124
  this.root = null;
911
1125
  this.trigger = null;
912
1126
  this.container = null;
1127
+ this.shellEl = null;
1128
+ this.bodyEl = null;
1129
+ this.tabRefs = {};
1130
+ this.bannerEl = null;
913
1131
  }
914
1132
  open() {
915
1133
  this.state.open = true;
@@ -1018,24 +1236,39 @@ var MaltoWidget = class {
1018
1236
  if (!this.root) return;
1019
1237
  if (this.resolvedMode === "inline") {
1020
1238
  if (!this.container) return;
1021
- this.container.innerHTML = "";
1022
- 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();
1023
1245
  return;
1024
1246
  }
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);
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();
1037
1270
  }
1038
- buildPanelBody() {
1271
+ buildShell() {
1039
1272
  const wrap = document.createElement("div");
1040
1273
  wrap.style.display = "flex";
1041
1274
  wrap.style.flexDirection = "column";
@@ -1045,23 +1278,39 @@ var MaltoWidget = class {
1045
1278
  wrap.appendChild(this.buildTabs());
1046
1279
  const body = document.createElement("div");
1047
1280
  body.className = "malto-body";
1048
- body.appendChild(this.buildView());
1281
+ this.bodyEl = body;
1049
1282
  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
- }
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);
1063
1296
  return wrap;
1064
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
+ }
1065
1314
  buildHeader() {
1066
1315
  const header = document.createElement("div");
1067
1316
  header.className = "malto-header";
@@ -1098,14 +1347,14 @@ var MaltoWidget = class {
1098
1347
  buildTabs() {
1099
1348
  const tabs = document.createElement("div");
1100
1349
  tabs.className = "malto-tabs";
1350
+ this.tabRefs = {};
1101
1351
  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
1352
  if (enabled.includes("roadmap"))
1106
1353
  tabs.appendChild(this.tabBtn("Roadmap", "roadmap"));
1107
1354
  if (enabled.includes("changelog"))
1108
1355
  tabs.appendChild(this.tabBtn("Updates", "changelog"));
1356
+ if (enabled.includes("submit"))
1357
+ tabs.appendChild(this.tabBtn("New", "submit"));
1109
1358
  return tabs;
1110
1359
  }
1111
1360
  tabBtn(label, view) {
@@ -1117,13 +1366,16 @@ var MaltoWidget = class {
1117
1366
  );
1118
1367
  btn.textContent = label;
1119
1368
  btn.addEventListener("click", () => this.go(view));
1369
+ this.tabRefs[view] = btn;
1120
1370
  return btn;
1121
1371
  }
1122
1372
  go(view) {
1123
1373
  this.state.view = view;
1124
1374
  this.state.error = null;
1375
+ if (view === "roadmap" || view === "changelog" || view === "detail" && this.state.selectedFeedback) {
1376
+ this.state.loading = true;
1377
+ }
1125
1378
  this.render();
1126
- if (view === "list") void this.loadFeedbacks();
1127
1379
  if (view === "roadmap") void this.loadRoadmap();
1128
1380
  if (view === "changelog") void this.loadReleases();
1129
1381
  if (view === "detail" && this.state.selectedFeedback) {
@@ -1131,21 +1383,12 @@ var MaltoWidget = class {
1131
1383
  }
1132
1384
  }
1133
1385
  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));
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
+ );
1149
1392
  }
1150
1393
  buildView() {
1151
1394
  const wrap = document.createElement("div");
@@ -1156,12 +1399,6 @@ var MaltoWidget = class {
1156
1399
  wrap.appendChild(e);
1157
1400
  }
1158
1401
  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
1402
  case "submit":
1166
1403
  wrap.appendChild(this.viewSubmit());
1167
1404
  break;
@@ -1170,15 +1407,9 @@ var MaltoWidget = class {
1170
1407
  break;
1171
1408
  case "roadmap":
1172
1409
  wrap.appendChild(this.viewRoadmap());
1173
- if (Object.keys(this.state.roadmap).length === 0 && !this.state.loading) {
1174
- void this.loadRoadmap();
1175
- }
1176
1410
  break;
1177
1411
  case "changelog":
1178
1412
  wrap.appendChild(this.viewChangelog());
1179
- if (this.state.releases.length === 0 && !this.state.loading) {
1180
- void this.loadReleases();
1181
- }
1182
1413
  break;
1183
1414
  case "detail":
1184
1415
  wrap.appendChild(this.viewDetail());
@@ -1186,30 +1417,6 @@ var MaltoWidget = class {
1186
1417
  }
1187
1418
  return wrap;
1188
1419
  }
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
1420
  feedbackRow(fb) {
1214
1421
  const row = document.createElement("div");
1215
1422
  row.className = "malto-item";
@@ -1289,8 +1496,7 @@ var MaltoWidget = class {
1289
1496
  });
1290
1497
  titleInput.value = "";
1291
1498
  descInput.value = "";
1292
- await this.loadFeedbacks();
1293
- this.go("list");
1499
+ this.go("roadmap");
1294
1500
  } catch (err) {
1295
1501
  this.state.error = err.message;
1296
1502
  submit.removeAttribute("disabled");
@@ -1314,7 +1520,7 @@ var MaltoWidget = class {
1314
1520
  clearSession(this.apiKeyPrefix);
1315
1521
  this.state.email = null;
1316
1522
  this.state.name = null;
1317
- this.go("list");
1523
+ this.go("roadmap");
1318
1524
  });
1319
1525
  wrap.appendChild(p);
1320
1526
  wrap.appendChild(out);
@@ -1349,7 +1555,7 @@ var MaltoWidget = class {
1349
1555
  this.state.email = session.user.email;
1350
1556
  this.state.name = session.user.name;
1351
1557
  this.state.authStatus = "idle";
1352
- this.go("list");
1558
+ this.go("roadmap");
1353
1559
  } catch (err) {
1354
1560
  this.state.error = err.message;
1355
1561
  verifyBtn.removeAttribute("disabled");
@@ -1410,40 +1616,57 @@ var MaltoWidget = class {
1410
1616
  }
1411
1617
  viewRoadmap() {
1412
1618
  const wrap = document.createElement("div");
1619
+ wrap.className = "malto-kanban";
1413
1620
  if (this.state.loading) {
1414
- 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
+ }
1415
1631
  return wrap;
1416
1632
  }
1417
- const labels = {
1418
- PLANNED: "Planned",
1419
- IN_PROGRESS: "In Progress",
1420
- COMPLETED: "Done"
1421
- };
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
+ ];
1422
1639
  let any = false;
1423
- for (const status of ["PLANNED", "IN_PROGRESS", "COMPLETED"]) {
1640
+ for (const { status, title } of cols) {
1424
1641
  const items = this.state.roadmap[status] ?? [];
1642
+ if (items.length > 0) any = true;
1425
1643
  const col = document.createElement("div");
1426
- col.className = "malto-roadmap-col";
1644
+ col.className = "malto-kanban-col";
1645
+ const head = document.createElement("div");
1646
+ head.className = "malto-kanban-head";
1427
1647
  const h = document.createElement("h5");
1428
- h.textContent = labels[status] ?? status;
1648
+ h.className = "malto-kanban-title";
1649
+ h.textContent = title;
1429
1650
  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);
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";
1434
1658
  if (items.length === 0) {
1435
1659
  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);
1660
+ empty.className = "malto-kanban-empty";
1661
+ empty.textContent = "Nothing here.";
1662
+ list.appendChild(empty);
1440
1663
  } else {
1441
- any = true;
1442
- for (const fb of items) col.appendChild(this.feedbackRow(fb));
1664
+ for (const fb of items) list.appendChild(this.feedbackCard(fb));
1443
1665
  }
1666
+ col.appendChild(list);
1444
1667
  wrap.appendChild(col);
1445
1668
  }
1446
- if (!any && Object.keys(this.state.roadmap).length === 0) {
1669
+ if (!any) {
1447
1670
  wrap.innerHTML = "";
1448
1671
  wrap.appendChild(
1449
1672
  this.emptyState(
@@ -1455,6 +1678,102 @@ var MaltoWidget = class {
1455
1678
  }
1456
1679
  return wrap;
1457
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
+ }
1458
1777
  viewChangelog() {
1459
1778
  const wrap = document.createElement("div");
1460
1779
  if (this.state.loading) {
@@ -1491,18 +1810,20 @@ var MaltoWidget = class {
1491
1810
  const fb = this.state.selectedFeedback;
1492
1811
  if (!fb) {
1493
1812
  wrap.appendChild(
1494
- this.emptyState("\u{1F4ED}", "Nothing selected", "Pick a request from Feed.")
1813
+ this.emptyState("\u{1F4ED}", "Nothing selected", "Pick a request from the roadmap.")
1495
1814
  );
1496
1815
  return wrap;
1497
1816
  }
1498
1817
  const back = document.createElement("button");
1499
1818
  back.className = "malto-back";
1500
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';
1501
- back.addEventListener("click", () => this.go("list"));
1820
+ back.addEventListener("click", () => this.go("roadmap"));
1502
1821
  wrap.appendChild(back);
1503
1822
  wrap.appendChild(this.feedbackRow(fb));
1504
- const allowed = this.allowedViews();
1505
- if (allowed.includes("comment")) {
1823
+ const commentEnabled = this.board?.widget.enabledFeatures.includes(
1824
+ "comment"
1825
+ ) ?? true;
1826
+ if (commentEnabled) {
1506
1827
  const commentsHeader = document.createElement("h5");
1507
1828
  commentsHeader.textContent = "Comments";
1508
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;";
@@ -1636,18 +1957,6 @@ var MaltoWidget = class {
1636
1957
  wrap.appendChild(d);
1637
1958
  return wrap;
1638
1959
  }
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
1960
  async loadRoadmap() {
1652
1961
  this.state.loading = true;
1653
1962
  this.render();
@@ -1719,6 +2028,22 @@ function statusPill(status) {
1719
2028
  pill.appendChild(txt);
1720
2029
  return pill;
1721
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
+ }
1722
2047
  function formatCount(n) {
1723
2048
  if (n < 1e3) return String(n);
1724
2049
  if (n < 1e4) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";