@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.js CHANGED
@@ -176,10 +176,12 @@ function upsert(id, css) {
176
176
  function buildCss(opts) {
177
177
  const primary = opts.primary;
178
178
  const tones = derivePalette(primary);
179
- const radiusScale = opts.radius === "sm" ? "10px" : opts.radius === "lg" ? "22px" : "16px";
180
- const appearance = opts.appearance ?? "auto";
179
+ const radiusScale = opts.radius === "sm" ? "12px" : opts.radius === "lg" ? "24px" : "20px";
180
+ const appearance = opts.appearance ?? "light";
181
181
  const overrides = opts.cssVars ?? {};
182
182
  return `
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');
184
+
183
185
  :where(.malto-root, .malto-root *) {
184
186
  box-sizing: border-box;
185
187
  }
@@ -192,21 +194,21 @@ function buildCss(opts) {
192
194
  --malto-primary-600: ${tones.shade600};
193
195
  --malto-primary-rgb: ${tones.rgb};
194
196
  --malto-surface: ${overrides.surface ?? "#ffffff"};
195
- --malto-surface-muted: ${overrides.surfaceMuted ?? "#fbfaff"};
196
- --malto-surface-elevated: rgba(255,255,255,0.85);
197
- --malto-border: ${overrides.border ?? "rgba(15, 13, 35, 0.06)"};
198
- --malto-border-strong: rgba(15, 13, 35, 0.1);
199
- --malto-text: ${overrides.text ?? "#0f0d23"};
197
+ --malto-surface-muted: ${overrides.surfaceMuted ?? "#f2eeff"};
198
+ --malto-surface-elevated: rgba(255,255,255,0.92);
199
+ --malto-border: ${overrides.border ?? "#e2e8f0"};
200
+ --malto-border-strong: #cbd1de;
201
+ --malto-text: ${overrides.text ?? "#0a0a0a"};
200
202
  --malto-text-muted: ${overrides.textMuted ?? "#5b5871"};
201
- --malto-text-subtle: rgba(15, 13, 35, 0.45);
203
+ --malto-text-subtle: rgba(10, 10, 10, 0.5);
202
204
  --malto-radius: ${overrides.radius ?? radiusScale};
203
- --malto-radius-sm: 10px;
205
+ --malto-radius-sm: 12px;
204
206
  --malto-radius-pill: 999px;
205
- --malto-shadow-sm: ${overrides.shadow ?? "0 4px 12px -4px rgba(15, 13, 35, 0.08)"};
206
- --malto-shadow-md: 0 12px 32px -8px rgba(15, 13, 35, 0.16);
207
- --malto-shadow-lg: 0 30px 60px -20px rgba(15, 13, 35, 0.28);
208
- --malto-shadow-glow: 0 12px 24px -8px rgba(var(--malto-primary-rgb), 0.45);
209
- --malto-font: ${overrides.font ?? "-apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', Roboto, system-ui, sans-serif"};
207
+ --malto-shadow-sm: ${overrides.shadow ?? "0 4px 12px -6px rgba(25, 24, 56, 0.12)"};
208
+ --malto-shadow-md: 0 10px 30px -10px rgba(25, 24, 56, 0.15);
209
+ --malto-shadow-lg: 0 30px 60px -20px rgba(25, 24, 56, 0.25);
210
+ --malto-shadow-glow: 0 8px 20px -6px rgba(var(--malto-primary-rgb), 0.45);
211
+ --malto-font: ${overrides.font ?? "'Google Sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif"};
210
212
  --malto-ease-spring: cubic-bezier(0.34, 1.4, 0.5, 1);
211
213
  --malto-ease-smooth: cubic-bezier(0.32, 0.72, 0, 1);
212
214
  }
@@ -267,12 +269,12 @@ function buildCss(opts) {
267
269
 
268
270
  .malto-trigger {
269
271
  display: inline-flex; align-items: center; gap: 8px;
270
- padding: 9px 18px;
272
+ padding: 10px 20px;
271
273
  border-radius: var(--malto-radius-pill);
272
274
  background: var(--malto-primary);
273
275
  color: var(--malto-primary-contrast);
274
276
  border: none; cursor: pointer;
275
- font-size: 13px; font-weight: 600; letter-spacing: -0.01em;
277
+ font-size: 14px; font-weight: 600; letter-spacing: -0.01em;
276
278
  box-shadow: var(--malto-shadow-glow);
277
279
  transition: transform .25s var(--malto-ease-spring), box-shadow .2s ease;
278
280
  }
@@ -298,7 +300,8 @@ function buildCss(opts) {
298
300
  .malto-modal {
299
301
  background: var(--malto-surface);
300
302
  width: 100%;
301
- max-width: 460px;
303
+ max-width: 1152px;
304
+ height: 720px;
302
305
  max-height: 92vh;
303
306
  border-radius: var(--malto-radius) var(--malto-radius) 0 0;
304
307
  box-shadow: var(--malto-shadow-lg);
@@ -326,23 +329,23 @@ function buildCss(opts) {
326
329
  /* \u2500\u2500\u2500\u2500\u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500 */
327
330
  .malto-header {
328
331
  display: flex; align-items: center; justify-content: space-between;
329
- padding: 18px 22px 14px;
332
+ padding: 22px 24px 16px;
330
333
  position: relative;
331
334
  flex: none;
332
335
  }
333
336
  .malto-header h3 {
334
337
  margin: 0;
335
- font-size: 16px; font-weight: 600;
338
+ font-size: 17px; font-weight: 600;
336
339
  color: var(--malto-text);
337
- letter-spacing: -0.015em;
340
+ letter-spacing: -0.02em;
338
341
  }
339
342
  .malto-header-meta {
340
343
  display: flex; align-items: center; gap: 10px;
341
344
  }
342
345
  .malto-board-logo {
343
346
  width: 28px; height: 28px;
344
- border-radius: 8px;
345
- background: var(--malto-primary-100);
347
+ border-radius: 10px;
348
+ background: var(--malto-surface-muted);
346
349
  display: flex; align-items: center; justify-content: center;
347
350
  color: var(--malto-primary);
348
351
  font-size: 12px; font-weight: 700;
@@ -360,7 +363,7 @@ function buildCss(opts) {
360
363
  transition: background .15s ease, color .15s ease;
361
364
  }
362
365
  .malto-close:hover {
363
- background: var(--malto-primary-50);
366
+ background: var(--malto-surface-muted);
364
367
  color: var(--malto-text);
365
368
  }
366
369
 
@@ -368,8 +371,8 @@ function buildCss(opts) {
368
371
  .malto-tabs {
369
372
  display: flex;
370
373
  padding: 4px;
371
- margin: 0 18px 14px;
372
- background: var(--malto-primary-50);
374
+ margin: 0 24px 16px;
375
+ background: var(--malto-surface-muted);
373
376
  border-radius: var(--malto-radius-sm);
374
377
  flex: none;
375
378
  }
@@ -395,7 +398,7 @@ function buildCss(opts) {
395
398
  .malto-body {
396
399
  flex: 1;
397
400
  overflow-y: auto;
398
- padding: 4px 18px 18px;
401
+ padding: 4px 24px 22px;
399
402
  background: var(--malto-surface);
400
403
  scroll-behavior: smooth;
401
404
  }
@@ -410,7 +413,7 @@ function buildCss(opts) {
410
413
  /* \u2500\u2500\u2500\u2500\u2500\u2500 Feedback row \u2500\u2500\u2500\u2500\u2500\u2500 */
411
414
  .malto-item {
412
415
  display: flex; gap: 12px;
413
- padding: 12px;
416
+ padding: 14px;
414
417
  border-radius: 12px;
415
418
  background: var(--malto-surface);
416
419
  border: 1px solid var(--malto-border);
@@ -421,15 +424,15 @@ function buildCss(opts) {
421
424
  .malto-item:hover {
422
425
  border-color: var(--malto-border-strong);
423
426
  transform: translateY(-1px);
424
- box-shadow: var(--malto-shadow-sm);
427
+ box-shadow: 0 6px 16px -8px rgba(25, 24, 56, 0.12);
425
428
  }
426
429
  .malto-vote {
427
430
  display: flex; flex-direction: column;
428
431
  align-items: center; justify-content: center;
429
432
  min-width: 44px; padding: 6px 4px;
430
- border-radius: 10px;
433
+ border-radius: 12px;
431
434
  border: 1px solid var(--malto-border);
432
- background: var(--malto-surface-muted);
435
+ background: var(--malto-surface);
433
436
  cursor: pointer;
434
437
  font-size: 11px; color: var(--malto-text-muted);
435
438
  transition: all .2s var(--malto-ease-spring);
@@ -516,10 +519,10 @@ function buildCss(opts) {
516
519
  }
517
520
  .malto-input, .malto-textarea {
518
521
  width: 100%;
519
- padding: 11px 14px;
520
- border: 1px solid var(--malto-border-strong);
522
+ padding: 12px 14px;
523
+ border: 1px solid var(--malto-border);
521
524
  border-radius: 12px;
522
- font-size: 13.5px;
525
+ font-size: 14px;
523
526
  color: var(--malto-text);
524
527
  background: var(--malto-surface);
525
528
  outline: none;
@@ -531,7 +534,7 @@ function buildCss(opts) {
531
534
  }
532
535
  .malto-input:focus, .malto-textarea:focus {
533
536
  border-color: var(--malto-primary);
534
- box-shadow: 0 0 0 4px rgba(var(--malto-primary-rgb), 0.1);
537
+ box-shadow: 0 0 0 4px rgba(var(--malto-primary-rgb), 0.12);
535
538
  }
536
539
  .malto-textarea {
537
540
  min-height: 110px;
@@ -539,13 +542,13 @@ function buildCss(opts) {
539
542
  line-height: 1.5;
540
543
  }
541
544
  .malto-button {
542
- padding: 11px 18px;
545
+ padding: 12px 20px;
543
546
  border-radius: 12px;
544
547
  background: var(--malto-primary);
545
548
  color: var(--malto-primary-contrast);
546
549
  border: none; cursor: pointer;
547
- font-size: 13px; font-weight: 600;
548
- letter-spacing: -0.005em;
550
+ font-size: 14px; font-weight: 600;
551
+ letter-spacing: -0.01em;
549
552
  box-shadow: var(--malto-shadow-glow);
550
553
  transition: transform .2s var(--malto-ease-spring), box-shadow .2s ease, opacity .2s ease;
551
554
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
@@ -554,13 +557,13 @@ function buildCss(opts) {
554
557
  .malto-button:active { transform: scale(0.97); }
555
558
  .malto-button[disabled] { opacity: .5; cursor: not-allowed; transform: none; }
556
559
  .malto-secondary {
557
- padding: 9px 14px;
558
- border-radius: 10px;
560
+ padding: 10px 16px;
561
+ border-radius: 12px;
559
562
  background: transparent;
560
563
  color: var(--malto-text);
561
- border: 1px solid var(--malto-border-strong);
564
+ border: 1px solid var(--malto-border);
562
565
  cursor: pointer;
563
- font-size: 13px; font-weight: 500;
566
+ font-size: 13.5px; font-weight: 500;
564
567
  transition: all .2s ease;
565
568
  }
566
569
  .malto-secondary:hover {
@@ -653,7 +656,7 @@ function buildCss(opts) {
653
656
 
654
657
  /* \u2500\u2500\u2500\u2500\u2500\u2500 Releases / changelog \u2500\u2500\u2500\u2500\u2500\u2500 */
655
658
  .malto-release {
656
- padding: 14px;
659
+ padding: 16px;
657
660
  border: 1px solid var(--malto-border);
658
661
  border-radius: 12px;
659
662
  margin-bottom: 8px;
@@ -723,10 +726,10 @@ function buildCss(opts) {
723
726
  /* \u2500\u2500\u2500\u2500\u2500\u2500 Footer banner (sign-in CTA) \u2500\u2500\u2500\u2500\u2500\u2500 */
724
727
  .malto-footer-banner {
725
728
  flex: none;
726
- padding: 12px 18px;
729
+ padding: 14px 24px;
727
730
  background: var(--malto-surface-muted);
728
731
  border-top: 1px solid var(--malto-border);
729
- font-size: 12px;
732
+ font-size: 12.5px;
730
733
  color: var(--malto-text-muted);
731
734
  display: flex; align-items: center; justify-content: space-between;
732
735
  gap: 10px;
@@ -746,7 +749,207 @@ function buildCss(opts) {
746
749
  }
747
750
  .malto-back:hover {
748
751
  color: var(--malto-text);
749
- background: var(--malto-primary-50);
752
+ background: var(--malto-surface-muted);
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);
750
953
  }
751
954
 
752
955
  /* \u2500\u2500\u2500\u2500\u2500\u2500 Animations \u2500\u2500\u2500\u2500\u2500\u2500 */
@@ -838,6 +1041,11 @@ var MaltoWidget = class {
838
1041
  this.container = null;
839
1042
  this.trigger = null;
840
1043
  this.board = null;
1044
+ this.shellEl = null;
1045
+ this.bodyEl = null;
1046
+ this.tabRefs = {};
1047
+ this.bannerEl = null;
1048
+ this.initialLoaded = false;
841
1049
  if (!config.apiKey) throw new Error("Malto: apiKey is required");
842
1050
  this.apiKeyPrefix = config.apiKey.slice(0, 16);
843
1051
  this.config = {
@@ -850,7 +1058,7 @@ var MaltoWidget = class {
850
1058
  cssVars: config.cssVars,
851
1059
  customCss: config.customCss,
852
1060
  radius: config.radius ?? "md",
853
- appearance: config.appearance ?? "auto",
1061
+ appearance: config.appearance ?? "light",
854
1062
  zIndex: config.zIndex ?? 2147483600,
855
1063
  views: config.views,
856
1064
  identify: config.identify,
@@ -861,10 +1069,9 @@ var MaltoWidget = class {
861
1069
  const session = readSession(this.apiKeyPrefix);
862
1070
  this.state = {
863
1071
  open: false,
864
- view: "list",
865
- loading: false,
1072
+ view: "roadmap",
1073
+ loading: true,
866
1074
  error: null,
867
- feedbacks: [],
868
1075
  roadmap: {},
869
1076
  releases: [],
870
1077
  selectedFeedback: null,
@@ -897,17 +1104,28 @@ var MaltoWidget = class {
897
1104
  await this.runIdentify(this.config.identify);
898
1105
  }
899
1106
  this.renderHost(this.resolvedMode);
1107
+ this.ensureInitialLoad();
900
1108
  this.config.onReady?.();
901
1109
  } catch (err) {
902
1110
  this.config.onError?.(err);
903
1111
  }
904
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
+ }
905
1119
  unmount() {
906
1120
  this.root?.parentElement?.removeChild(this.root);
907
1121
  this.trigger?.parentElement?.removeChild(this.trigger);
908
1122
  this.root = null;
909
1123
  this.trigger = null;
910
1124
  this.container = null;
1125
+ this.shellEl = null;
1126
+ this.bodyEl = null;
1127
+ this.tabRefs = {};
1128
+ this.bannerEl = null;
911
1129
  }
912
1130
  open() {
913
1131
  this.state.open = true;
@@ -1016,24 +1234,39 @@ var MaltoWidget = class {
1016
1234
  if (!this.root) return;
1017
1235
  if (this.resolvedMode === "inline") {
1018
1236
  if (!this.container) return;
1019
- this.container.innerHTML = "";
1020
- 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();
1021
1243
  return;
1022
1244
  }
1023
- this.root.innerHTML = "";
1024
- if (!this.state.open) return;
1025
- const overlay = document.createElement("div");
1026
- overlay.className = "malto-overlay";
1027
- overlay.addEventListener("click", (e) => {
1028
- if (e.target === overlay) this.close();
1029
- });
1030
- const modal = document.createElement("div");
1031
- modal.className = "malto-modal";
1032
- modal.appendChild(this.buildPanelBody());
1033
- overlay.appendChild(modal);
1034
- 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();
1035
1268
  }
1036
- buildPanelBody() {
1269
+ buildShell() {
1037
1270
  const wrap = document.createElement("div");
1038
1271
  wrap.style.display = "flex";
1039
1272
  wrap.style.flexDirection = "column";
@@ -1043,23 +1276,39 @@ var MaltoWidget = class {
1043
1276
  wrap.appendChild(this.buildTabs());
1044
1277
  const body = document.createElement("div");
1045
1278
  body.className = "malto-body";
1046
- body.appendChild(this.buildView());
1279
+ this.bodyEl = body;
1047
1280
  wrap.appendChild(body);
1048
- if (this.state.view !== "auth" && !this.state.email) {
1049
- const banner = document.createElement("div");
1050
- banner.className = "malto-footer-banner";
1051
- const txt = document.createElement("span");
1052
- txt.innerHTML = `Sign in to vote, comment, or submit ideas.`;
1053
- const link = document.createElement("button");
1054
- link.className = "malto-link";
1055
- link.textContent = "Sign in";
1056
- link.addEventListener("click", () => this.go("auth"));
1057
- banner.appendChild(txt);
1058
- banner.appendChild(link);
1059
- wrap.appendChild(banner);
1060
- }
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);
1061
1294
  return wrap;
1062
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
+ }
1063
1312
  buildHeader() {
1064
1313
  const header = document.createElement("div");
1065
1314
  header.className = "malto-header";
@@ -1096,14 +1345,14 @@ var MaltoWidget = class {
1096
1345
  buildTabs() {
1097
1346
  const tabs = document.createElement("div");
1098
1347
  tabs.className = "malto-tabs";
1348
+ this.tabRefs = {};
1099
1349
  const enabled = this.allowedViews();
1100
- if (enabled.includes("list")) tabs.appendChild(this.tabBtn("Feed", "list"));
1101
- if (enabled.includes("submit"))
1102
- tabs.appendChild(this.tabBtn("New", "submit"));
1103
1350
  if (enabled.includes("roadmap"))
1104
1351
  tabs.appendChild(this.tabBtn("Roadmap", "roadmap"));
1105
1352
  if (enabled.includes("changelog"))
1106
1353
  tabs.appendChild(this.tabBtn("Updates", "changelog"));
1354
+ if (enabled.includes("submit"))
1355
+ tabs.appendChild(this.tabBtn("New", "submit"));
1107
1356
  return tabs;
1108
1357
  }
1109
1358
  tabBtn(label, view) {
@@ -1115,13 +1364,16 @@ var MaltoWidget = class {
1115
1364
  );
1116
1365
  btn.textContent = label;
1117
1366
  btn.addEventListener("click", () => this.go(view));
1367
+ this.tabRefs[view] = btn;
1118
1368
  return btn;
1119
1369
  }
1120
1370
  go(view) {
1121
1371
  this.state.view = view;
1122
1372
  this.state.error = null;
1373
+ if (view === "roadmap" || view === "changelog" || view === "detail" && this.state.selectedFeedback) {
1374
+ this.state.loading = true;
1375
+ }
1123
1376
  this.render();
1124
- if (view === "list") void this.loadFeedbacks();
1125
1377
  if (view === "roadmap") void this.loadRoadmap();
1126
1378
  if (view === "changelog") void this.loadReleases();
1127
1379
  if (view === "detail" && this.state.selectedFeedback) {
@@ -1129,21 +1381,12 @@ var MaltoWidget = class {
1129
1381
  }
1130
1382
  }
1131
1383
  allowedViews() {
1132
- const settings = this.board?.widget.enabledFeatures ?? [
1133
- "list",
1134
- "submit",
1135
- "vote",
1136
- "comment",
1137
- "roadmap",
1138
- "changelog"
1139
- ];
1140
- const requested = this.config.views ?? [
1141
- "list",
1142
- "submit",
1143
- "roadmap",
1144
- "changelog"
1145
- ];
1146
- 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
+ );
1147
1390
  }
1148
1391
  buildView() {
1149
1392
  const wrap = document.createElement("div");
@@ -1154,12 +1397,6 @@ var MaltoWidget = class {
1154
1397
  wrap.appendChild(e);
1155
1398
  }
1156
1399
  switch (this.state.view) {
1157
- case "list":
1158
- wrap.appendChild(this.viewList());
1159
- if (this.state.feedbacks.length === 0 && !this.state.loading) {
1160
- void this.loadFeedbacks();
1161
- }
1162
- break;
1163
1400
  case "submit":
1164
1401
  wrap.appendChild(this.viewSubmit());
1165
1402
  break;
@@ -1168,15 +1405,9 @@ var MaltoWidget = class {
1168
1405
  break;
1169
1406
  case "roadmap":
1170
1407
  wrap.appendChild(this.viewRoadmap());
1171
- if (Object.keys(this.state.roadmap).length === 0 && !this.state.loading) {
1172
- void this.loadRoadmap();
1173
- }
1174
1408
  break;
1175
1409
  case "changelog":
1176
1410
  wrap.appendChild(this.viewChangelog());
1177
- if (this.state.releases.length === 0 && !this.state.loading) {
1178
- void this.loadReleases();
1179
- }
1180
1411
  break;
1181
1412
  case "detail":
1182
1413
  wrap.appendChild(this.viewDetail());
@@ -1184,30 +1415,6 @@ var MaltoWidget = class {
1184
1415
  }
1185
1416
  return wrap;
1186
1417
  }
1187
- viewList() {
1188
- const wrap = document.createElement("div");
1189
- if (this.state.loading) {
1190
- wrap.appendChild(this.skeletonList(4));
1191
- return wrap;
1192
- }
1193
- if (this.state.feedbacks.length === 0) {
1194
- wrap.appendChild(
1195
- this.emptyState(
1196
- "\u{1F4A1}",
1197
- "No requests yet",
1198
- "Be the first to share an idea or report something."
1199
- )
1200
- );
1201
- return wrap;
1202
- }
1203
- const list = document.createElement("div");
1204
- list.className = "malto-list";
1205
- for (const fb of this.state.feedbacks) {
1206
- list.appendChild(this.feedbackRow(fb));
1207
- }
1208
- wrap.appendChild(list);
1209
- return wrap;
1210
- }
1211
1418
  feedbackRow(fb) {
1212
1419
  const row = document.createElement("div");
1213
1420
  row.className = "malto-item";
@@ -1287,8 +1494,7 @@ var MaltoWidget = class {
1287
1494
  });
1288
1495
  titleInput.value = "";
1289
1496
  descInput.value = "";
1290
- await this.loadFeedbacks();
1291
- this.go("list");
1497
+ this.go("roadmap");
1292
1498
  } catch (err) {
1293
1499
  this.state.error = err.message;
1294
1500
  submit.removeAttribute("disabled");
@@ -1312,7 +1518,7 @@ var MaltoWidget = class {
1312
1518
  clearSession(this.apiKeyPrefix);
1313
1519
  this.state.email = null;
1314
1520
  this.state.name = null;
1315
- this.go("list");
1521
+ this.go("roadmap");
1316
1522
  });
1317
1523
  wrap.appendChild(p);
1318
1524
  wrap.appendChild(out);
@@ -1347,7 +1553,7 @@ var MaltoWidget = class {
1347
1553
  this.state.email = session.user.email;
1348
1554
  this.state.name = session.user.name;
1349
1555
  this.state.authStatus = "idle";
1350
- this.go("list");
1556
+ this.go("roadmap");
1351
1557
  } catch (err) {
1352
1558
  this.state.error = err.message;
1353
1559
  verifyBtn.removeAttribute("disabled");
@@ -1408,40 +1614,57 @@ var MaltoWidget = class {
1408
1614
  }
1409
1615
  viewRoadmap() {
1410
1616
  const wrap = document.createElement("div");
1617
+ wrap.className = "malto-kanban";
1411
1618
  if (this.state.loading) {
1412
- 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
+ }
1413
1629
  return wrap;
1414
1630
  }
1415
- const labels = {
1416
- PLANNED: "Planned",
1417
- IN_PROGRESS: "In Progress",
1418
- COMPLETED: "Done"
1419
- };
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
+ ];
1420
1637
  let any = false;
1421
- for (const status of ["PLANNED", "IN_PROGRESS", "COMPLETED"]) {
1638
+ for (const { status, title } of cols) {
1422
1639
  const items = this.state.roadmap[status] ?? [];
1640
+ if (items.length > 0) any = true;
1423
1641
  const col = document.createElement("div");
1424
- col.className = "malto-roadmap-col";
1642
+ col.className = "malto-kanban-col";
1643
+ const head = document.createElement("div");
1644
+ head.className = "malto-kanban-head";
1425
1645
  const h = document.createElement("h5");
1426
- h.textContent = labels[status] ?? status;
1646
+ h.className = "malto-kanban-title";
1647
+ h.textContent = title;
1427
1648
  const c = document.createElement("span");
1428
- c.className = "malto-roadmap-count";
1429
- c.textContent = String(items.length);
1430
- h.appendChild(c);
1431
- 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";
1432
1656
  if (items.length === 0) {
1433
1657
  const empty = document.createElement("div");
1434
- empty.className = "malto-empty-desc";
1435
- empty.style.padding = "8px 0 12px";
1436
- empty.textContent = "Nothing here yet.";
1437
- col.appendChild(empty);
1658
+ empty.className = "malto-kanban-empty";
1659
+ empty.textContent = "Nothing here.";
1660
+ list.appendChild(empty);
1438
1661
  } else {
1439
- any = true;
1440
- for (const fb of items) col.appendChild(this.feedbackRow(fb));
1662
+ for (const fb of items) list.appendChild(this.feedbackCard(fb));
1441
1663
  }
1664
+ col.appendChild(list);
1442
1665
  wrap.appendChild(col);
1443
1666
  }
1444
- if (!any && Object.keys(this.state.roadmap).length === 0) {
1667
+ if (!any) {
1445
1668
  wrap.innerHTML = "";
1446
1669
  wrap.appendChild(
1447
1670
  this.emptyState(
@@ -1453,6 +1676,102 @@ var MaltoWidget = class {
1453
1676
  }
1454
1677
  return wrap;
1455
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
+ }
1456
1775
  viewChangelog() {
1457
1776
  const wrap = document.createElement("div");
1458
1777
  if (this.state.loading) {
@@ -1489,18 +1808,20 @@ var MaltoWidget = class {
1489
1808
  const fb = this.state.selectedFeedback;
1490
1809
  if (!fb) {
1491
1810
  wrap.appendChild(
1492
- this.emptyState("\u{1F4ED}", "Nothing selected", "Pick a request from Feed.")
1811
+ this.emptyState("\u{1F4ED}", "Nothing selected", "Pick a request from the roadmap.")
1493
1812
  );
1494
1813
  return wrap;
1495
1814
  }
1496
1815
  const back = document.createElement("button");
1497
1816
  back.className = "malto-back";
1498
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';
1499
- back.addEventListener("click", () => this.go("list"));
1818
+ back.addEventListener("click", () => this.go("roadmap"));
1500
1819
  wrap.appendChild(back);
1501
1820
  wrap.appendChild(this.feedbackRow(fb));
1502
- const allowed = this.allowedViews();
1503
- if (allowed.includes("comment")) {
1821
+ const commentEnabled = this.board?.widget.enabledFeatures.includes(
1822
+ "comment"
1823
+ ) ?? true;
1824
+ if (commentEnabled) {
1504
1825
  const commentsHeader = document.createElement("h5");
1505
1826
  commentsHeader.textContent = "Comments";
1506
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;";
@@ -1634,18 +1955,6 @@ var MaltoWidget = class {
1634
1955
  wrap.appendChild(d);
1635
1956
  return wrap;
1636
1957
  }
1637
- async loadFeedbacks() {
1638
- this.state.loading = true;
1639
- this.render();
1640
- try {
1641
- this.state.feedbacks = await this.client.listFeedbacks();
1642
- } catch (err) {
1643
- this.state.error = err.message;
1644
- } finally {
1645
- this.state.loading = false;
1646
- this.render();
1647
- }
1648
- }
1649
1958
  async loadRoadmap() {
1650
1959
  this.state.loading = true;
1651
1960
  this.render();
@@ -1717,6 +2026,22 @@ function statusPill(status) {
1717
2026
  pill.appendChild(txt);
1718
2027
  return pill;
1719
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
+ }
1720
2045
  function formatCount(n) {
1721
2046
  if (n < 1e3) return String(n);
1722
2047
  if (n < 1e4) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";