@designid/tokens 1.2.15 → 1.2.16

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.
@@ -0,0 +1,3009 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>@DesignID Tokens - Figma Sync</title>
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
15
+ font-size: 12px;
16
+ background: var(--figma-color-bg);
17
+ color: var(--figma-color-text);
18
+ overflow: hidden;
19
+ }
20
+
21
+ .container {
22
+ display: flex;
23
+ height: 100vh;
24
+ flex-direction: column;
25
+ }
26
+
27
+ .header {
28
+ padding: 12px 16px;
29
+ border-bottom: 1px solid var(--figma-color-border);
30
+ background: var(--figma-color-bg);
31
+ }
32
+
33
+ .header h1 {
34
+ margin: 0 0 8px 0;
35
+ font-size: 14px;
36
+ font-weight: 600;
37
+ }
38
+
39
+ .header p {
40
+ margin: 0;
41
+ font-size: 11px;
42
+ color: var(--figma-color-text-secondary);
43
+ }
44
+
45
+ .actions {
46
+ padding: 0 16px;
47
+ border-bottom: 1px solid var(--figma-color-border);
48
+ display: flex;
49
+ gap: 8px;
50
+ align-items: center;
51
+ justify-content: space-between;
52
+ }
53
+
54
+ .tabs {
55
+ display: flex;
56
+ gap: 4px;
57
+ }
58
+
59
+ .tab {
60
+ padding: 8px 16px;
61
+ border: none;
62
+ background: transparent;
63
+ color: var(--figma-color-text-secondary);
64
+ border-radius: 0px;
65
+ cursor: pointer;
66
+ font-size: 12px;
67
+ font-weight: 500;
68
+ transition: all 0.2s;
69
+ }
70
+
71
+ .tab:hover {
72
+ background: var(--figma-color-bg-hover);
73
+ color: var(--figma-color-text);
74
+ }
75
+
76
+ .tab.active {
77
+ background: var(--figma-color-bg-selected);
78
+ color: var(--figma-color-text-onselected);
79
+ }
80
+
81
+ #actions-menu-btn {
82
+ border: none;
83
+ }
84
+
85
+ .context-menu-btn {
86
+ position: relative;
87
+ }
88
+
89
+ .context-menu {
90
+ position: absolute;
91
+ top: 100%;
92
+ right: 0;
93
+ margin-top: 4px;
94
+ background: var(--figma-color-bg);
95
+ border-radius: 4px;
96
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
97
+ min-width: 200px;
98
+ z-index: 1000;
99
+ display: none;
100
+ }
101
+
102
+ .context-menu.open {
103
+ display: block;
104
+ }
105
+
106
+ .context-menu-item {
107
+ padding: 10px 16px;
108
+ cursor: pointer;
109
+ font-size: 12px;
110
+ transition: background 0.2s;
111
+ border: none;
112
+ background: transparent;
113
+ width: 100%;
114
+ text-align: left;
115
+ color: var(--figma-color-text);
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 8px;
119
+ }
120
+
121
+ .context-menu-item:hover {
122
+ background: var(--figma-color-bg-hover);
123
+ }
124
+
125
+ .context-menu-item:first-child {
126
+ border-radius: 4px 4px 0 0;
127
+ }
128
+
129
+ .context-menu-item:last-child {
130
+ border-radius: 0 0 4px 4px;
131
+ }
132
+
133
+ .context-menu-divider {
134
+ height: 1px;
135
+ background: var(--figma-color-border);
136
+ margin: 4px 0;
137
+ }
138
+
139
+ button {
140
+ padding: 8px 12px;
141
+ border: 1px solid var(--figma-color-border);
142
+ background: var(--figma-color-bg);
143
+ color: var(--figma-color-text);
144
+ border-radius: 4px;
145
+ cursor: pointer;
146
+ font-size: 11px;
147
+ font-weight: 500;
148
+ transition: background 0.2s;
149
+ }
150
+
151
+ button:hover {
152
+ background: var(--figma-color-bg-hover);
153
+ }
154
+
155
+ button:active {
156
+ background: var(--figma-color-bg-pressed);
157
+ }
158
+
159
+ button.primary {
160
+ background: var(--figma-color-bg-brand);
161
+ color: white;
162
+ border-color: var(--figma-color-bg-brand);
163
+ }
164
+
165
+ button.primary:hover {
166
+ background: var(--figma-color-bg-brand-hover);
167
+ }
168
+
169
+ button:disabled {
170
+ opacity: 0.5;
171
+ cursor: not-allowed;
172
+ }
173
+
174
+ .content {
175
+ flex: 1;
176
+ display: flex;
177
+ overflow: hidden;
178
+ }
179
+
180
+ .sidebar {
181
+ width: 280px;
182
+ border-right: 1px solid var(--figma-color-border);
183
+ display: flex;
184
+ flex-direction: column;
185
+ background: var(--figma-color-bg);
186
+ }
187
+
188
+ .search-box {
189
+ padding: 12px;
190
+ border-bottom: 1px solid var(--figma-color-border);
191
+ position: relative;
192
+ }
193
+
194
+ .search-box input {
195
+ width: 100%;
196
+ padding: 6px 28px 6px 8px;
197
+ border: 1px solid var(--figma-color-border);
198
+ border-radius: 4px;
199
+ background: var(--figma-color-bg);
200
+ color: var(--figma-color-text);
201
+ font-size: 11px;
202
+ }
203
+
204
+ .search-box input:focus {
205
+ outline: 2px solid var(--figma-color-bg-brand);
206
+ outline-offset: -1px;
207
+ }
208
+
209
+ .search-clear {
210
+ position: absolute;
211
+ right: 20px;
212
+ top: 50%;
213
+ transform: translateY(-50%);
214
+ padding: 2px 6px;
215
+ border: none;
216
+ background: transparent;
217
+ color: var(--figma-color-text-secondary);
218
+ cursor: pointer;
219
+ font-size: 16px;
220
+ line-height: 1;
221
+ display: none;
222
+ }
223
+
224
+ .search-clear.visible {
225
+ display: block;
226
+ }
227
+
228
+ .sidebar-controls {
229
+ padding: 8px 12px;
230
+ display: flex;
231
+ gap: 8px;
232
+ border-bottom: 1px solid var(--figma-color-border);
233
+ }
234
+
235
+ .sidebar-controls button {
236
+ flex: 1;
237
+ padding: 4px 8px;
238
+ font-size: 10px;
239
+ }
240
+
241
+ .token-tree {
242
+ flex: 1;
243
+ overflow-y: auto;
244
+ padding: 8px;
245
+ }
246
+
247
+ .tree-node {
248
+ user-select: none;
249
+ }
250
+
251
+ .tree-node-header {
252
+ display: flex;
253
+ align-items: center;
254
+ padding: 4px 8px;
255
+ cursor: pointer;
256
+ border-radius: 4px;
257
+ margin-bottom: 2px;
258
+ }
259
+
260
+ .tree-node-header:hover {
261
+ background: var(--figma-color-bg-hover);
262
+ }
263
+
264
+ .tree-node-header.selected {
265
+ background: var(--figma-color-bg-selected);
266
+ color: var(--figma-color-text-onselected);
267
+ }
268
+
269
+ .tree-node-icon {
270
+ width: 12px;
271
+ margin-right: 4px;
272
+ font-size: 10px;
273
+ color: var(--figma-color-text-secondary);
274
+ }
275
+
276
+ .tree-node-label {
277
+ flex: 1;
278
+ font-size: 11px;
279
+ }
280
+
281
+ .tree-node-type {
282
+ font-size: 9px;
283
+ color: var(--figma-color-text-secondary);
284
+ background: var(--figma-color-bg-secondary);
285
+ padding: 2px 6px;
286
+ border-radius: 3px;
287
+ margin-left: 4px;
288
+ }
289
+
290
+ .tree-node-value {
291
+ margin-left: 8px;
292
+ font-size: 11px;
293
+ color: #888;
294
+ font-family: 'Monaco', 'Menlo', monospace;
295
+ display: flex;
296
+ align-items: center;
297
+ gap: 6px;
298
+ }
299
+
300
+ .color-swatch {
301
+ width: 16px;
302
+ height: 16px;
303
+ border-radius: 50%;
304
+ border: 1px solid var(--figma-color-border);
305
+ display: inline-block;
306
+ flex-shrink: 0;
307
+ }
308
+
309
+ .color-swatch-group {
310
+ display: flex;
311
+ gap: 4px;
312
+ align-items: center;
313
+ }
314
+
315
+ .mode-swatch {
316
+ position: relative;
317
+ }
318
+
319
+ /* .mode-swatch::after {
320
+ content: attr(data-mode);
321
+ position: absolute;
322
+ bottom: -14px;
323
+ left: 50%;
324
+ transform: translateX(-50%);
325
+ font-size: 8px;
326
+ color: var(--figma-color-text-secondary);
327
+ white-space: nowrap;
328
+ } */
329
+
330
+ .token-reference-info {
331
+ margin-top: 4px;
332
+ padding: 4px 8px;
333
+ background: #e6f2ff;
334
+ border-radius: 4px;
335
+ }
336
+
337
+ .tree-node-children {
338
+ margin-left: 16px;
339
+ }
340
+
341
+ .tree-node-children.collapsed {
342
+ display: none;
343
+ }
344
+
345
+ .editor {
346
+ flex: 1;
347
+ padding: 16px;
348
+ overflow-y: auto;
349
+ }
350
+
351
+ .empty-state {
352
+ display: flex;
353
+ flex-direction: column;
354
+ align-items: center;
355
+ justify-content: center;
356
+ height: 100%;
357
+ text-align: center;
358
+ color: var(--figma-color-text-secondary);
359
+ }
360
+
361
+ .empty-state h3 {
362
+ margin: 0 0 8px 0;
363
+ font-size: 14px;
364
+ font-weight: 600;
365
+ color: var(--figma-color-text);
366
+ }
367
+
368
+ .empty-state p {
369
+ margin: 0 0 16px 0;
370
+ font-size: 11px;
371
+ }
372
+
373
+ .editor-form {
374
+ max-width: 500px;
375
+ }
376
+
377
+ .form-group {
378
+ margin-bottom: 16px;
379
+ }
380
+
381
+ .form-group label {
382
+ display: block;
383
+ margin-bottom: 4px;
384
+ font-size: 11px;
385
+ font-weight: 500;
386
+ color: var(--figma-color-text);
387
+ }
388
+
389
+ .form-group input,
390
+ .form-group select,
391
+ .form-group textarea {
392
+ width: 100%;
393
+ padding: 6px 8px;
394
+ border: 1px solid var(--figma-color-border);
395
+ border-radius: 4px;
396
+ background: var(--figma-color-bg);
397
+ color: var(--figma-color-text);
398
+ font-size: 11px;
399
+ font-family: inherit;
400
+ }
401
+
402
+ .form-group textarea {
403
+ min-height: 60px;
404
+ resize: vertical;
405
+ }
406
+
407
+ .form-group input:focus,
408
+ .form-group select:focus,
409
+ .form-group textarea:focus {
410
+ outline: 2px solid var(--figma-color-bg-brand);
411
+ outline-offset: -1px;
412
+ }
413
+
414
+ .form-actions {
415
+ display: flex;
416
+ gap: 8px;
417
+ margin-top: 16px;
418
+ }
419
+
420
+ .form-actions button {
421
+ flex: 1;
422
+ padding: 8px 16px;
423
+ border-radius: 4px;
424
+ cursor: pointer;
425
+ font-size: 11px;
426
+ font-weight: 500;
427
+ transition: all 0.2s;
428
+ }
429
+
430
+ .form-actions button.primary {
431
+ background: var(--figma-color-bg-brand);
432
+ color: white;
433
+ border: none;
434
+ }
435
+
436
+ .form-actions button.primary:hover {
437
+ opacity: 0.9;
438
+ }
439
+
440
+ .form-actions button:not(.primary) {
441
+ background: var(--figma-color-bg-secondary);
442
+ color: var(--figma-color-text);
443
+ border: 1px solid var(--figma-color-border);
444
+ }
445
+
446
+ .form-actions button:not(.primary):hover {
447
+ background: var(--figma-color-bg-hover);
448
+ }
449
+
450
+ .status-bar {
451
+ padding: 8px 16px;
452
+ border-top: 1px solid var(--figma-color-border);
453
+ font-size: 11px;
454
+ background: var(--figma-color-bg);
455
+ color: var(--figma-color-text-secondary);
456
+ }
457
+
458
+ .status-bar.success {
459
+ background: #0d8a00;
460
+ color: white;
461
+ }
462
+
463
+ .status-bar.error {
464
+ background: #f24822;
465
+ color: white;
466
+ }
467
+
468
+ .loading {
469
+ padding: 16px;
470
+ text-align: center;
471
+ color: var(--figma-color-text-secondary);
472
+ font-size: 11px;
473
+ }
474
+
475
+ .token-preview {
476
+ margin-top: 8px;
477
+ padding: 12px;
478
+ background: var(--figma-color-bg-secondary);
479
+ border-radius: 4px;
480
+ border: 1px solid var(--figma-color-border);
481
+ }
482
+
483
+ .token-preview-label {
484
+ font-size: 10px;
485
+ color: var(--figma-color-text-secondary);
486
+ margin-bottom: 8px;
487
+ }
488
+
489
+ .token-preview-color {
490
+ width: 100%;
491
+ height: 40px;
492
+ border-radius: 4px;
493
+ border: 1px solid var(--figma-color-border);
494
+ }
495
+
496
+ .token-preview-modes {
497
+ display: flex;
498
+ gap: 12px;
499
+ margin-top: 8px;
500
+ flex-wrap: wrap;
501
+ }
502
+
503
+ .token-preview-mode {
504
+ display: flex;
505
+ flex-direction: column;
506
+ align-items: center;
507
+ gap: 4px;
508
+ }
509
+
510
+ .token-preview-mode-label {
511
+ font-size: 9px;
512
+ color: var(--figma-color-text-secondary);
513
+ text-transform: uppercase;
514
+ }
515
+
516
+ .token-preview-mode-swatch {
517
+ width: 32px;
518
+ height: 32px;
519
+ border-radius: 50%;
520
+ border: 1px solid var(--figma-color-border);
521
+ }
522
+
523
+ .progress-bar {
524
+ width: 100%;
525
+ height: 4px;
526
+ background: var(--figma-color-bg-secondary);
527
+ border-radius: 2px;
528
+ overflow: hidden;
529
+ margin-top: 8px;
530
+ }
531
+
532
+ .progress-bar-fill {
533
+ height: 100%;
534
+ background: var(--figma-color-bg-brand);
535
+ transition: width 0.3s;
536
+ }
537
+
538
+ /* Files View Styles */
539
+ .files-view {
540
+ display: none;
541
+ flex-direction: column;
542
+ height: 100%;
543
+ }
544
+
545
+ .files-view.active {
546
+ display: flex;
547
+ }
548
+
549
+ .files-list {
550
+ flex: 1;
551
+ overflow-y: auto;
552
+ padding: 16px;
553
+ }
554
+
555
+ .file-item {
556
+ padding: 12px;
557
+ border: 1px solid var(--figma-color-border);
558
+ border-radius: 4px;
559
+ margin-bottom: 8px;
560
+ cursor: pointer;
561
+ transition: background 0.2s;
562
+ }
563
+
564
+ .file-item:hover {
565
+ background: var(--figma-color-bg-hover);
566
+ }
567
+
568
+ .file-item.selected {
569
+ background: var(--figma-color-bg-selected);
570
+ border-color: var(--figma-color-bg-brand);
571
+ }
572
+
573
+ .file-item.has-changes {
574
+ border-left: 3px solid var(--figma-color-bg-brand);
575
+ }
576
+
577
+ .file-header {
578
+ display: flex;
579
+ align-items: center;
580
+ justify-content: space-between;
581
+ }
582
+
583
+ .file-name {
584
+ font-size: 12px;
585
+ font-weight: 500;
586
+ color: var(--figma-color-text);
587
+ font-family: 'Monaco', 'Menlo', monospace;
588
+ }
589
+
590
+ .file-badge {
591
+ font-size: 9px;
592
+ padding: 2px 6px;
593
+ border-radius: 3px;
594
+ background: var(--figma-color-bg-brand);
595
+ color: white;
596
+ font-weight: 600;
597
+ }
598
+
599
+ .file-path {
600
+ font-size: 10px;
601
+ color: var(--figma-color-text-secondary);
602
+ margin-top: 4px;
603
+ }
604
+
605
+ .file-detail {
606
+ display: none;
607
+ flex-direction: column;
608
+ height: 100%;
609
+ background: var(--figma-color-bg);
610
+ }
611
+
612
+ .file-detail.active {
613
+ display: flex;
614
+ }
615
+
616
+ .file-detail-header {
617
+ padding: 16px;
618
+ border-bottom: 1px solid var(--figma-color-border);
619
+ display: flex;
620
+ align-items: center;
621
+ justify-content: space-between;
622
+ }
623
+
624
+ .file-detail-title {
625
+ flex: 1;
626
+ }
627
+
628
+ .file-detail-title h3 {
629
+ margin: 0 0 4px 0;
630
+ font-size: 14px;
631
+ font-weight: 600;
632
+ }
633
+
634
+ .file-detail-title p {
635
+ margin: 0;
636
+ font-size: 11px;
637
+ color: var(--figma-color-text-secondary);
638
+ font-family: 'Monaco', 'Menlo', monospace;
639
+ }
640
+
641
+ .file-detail-actions {
642
+ display: flex;
643
+ gap: 8px;
644
+ }
645
+
646
+ .file-detail-content {
647
+ flex: 1;
648
+ overflow-y: auto;
649
+ padding: 16px;
650
+ }
651
+
652
+ .diff-section {
653
+ margin-bottom: 24px;
654
+ }
655
+
656
+ .diff-section h4 {
657
+ margin: 0 0 8px 0;
658
+ font-size: 12px;
659
+ font-weight: 600;
660
+ color: var(--figma-color-text-secondary);
661
+ }
662
+
663
+ .diff-item {
664
+ margin-bottom: 12px;
665
+ padding: 8px;
666
+ background: var(--figma-color-bg-secondary);
667
+ border-radius: 4px;
668
+ border-left: 3px solid var(--figma-color-border);
669
+ }
670
+
671
+ .diff-item.changed {
672
+ border-left-color: #ffcd29;
673
+ }
674
+
675
+ .diff-item.added {
676
+ border-left-color: #14ae5c;
677
+ }
678
+
679
+ .diff-item.removed {
680
+ border-left-color: #f24822;
681
+ }
682
+
683
+ .diff-path {
684
+ font-size: 11px;
685
+ font-weight: 600;
686
+ color: var(--figma-color-text);
687
+ margin-bottom: 4px;
688
+ font-family: 'Monaco', 'Menlo', monospace;
689
+ }
690
+
691
+ .diff-values {
692
+ display: flex;
693
+ gap: 12px;
694
+ font-size: 11px;
695
+ font-family: 'Monaco', 'Menlo', monospace;
696
+ }
697
+
698
+ .diff-old {
699
+ flex: 1;
700
+ color: #f24822;
701
+ }
702
+
703
+ .diff-new {
704
+ flex: 1;
705
+ color: #14ae5c;
706
+ }
707
+
708
+ .diff-label {
709
+ font-size: 9px;
710
+ text-transform: uppercase;
711
+ color: var(--figma-color-text-secondary);
712
+ margin-bottom: 2px;
713
+ }
714
+
715
+ /* Find & Replace Modal */
716
+ #find-replace-modal.active {
717
+ display: flex !important;
718
+ }
719
+ </style>
720
+ </head>
721
+ <body>
722
+ <div class="container">
723
+ <div class="header">
724
+ <h1>@DesignID Tokens - Figma Sync</h1>
725
+ <p>Synchronize design tokens with Figma variables, styles, and effects <span id="modes-indicator" style="color: #0066ff; font-weight: 600;"></span></p>
726
+ </div>
727
+
728
+ <div class="actions">
729
+ <div class="tabs">
730
+ <button class="tab active" id="tokens-tab">🎨 Tokens</button>
731
+ <button class="tab" id="files-tab">📄 Files</button>
732
+ <button class="tab" id="config-tab">⚙️ Config</button>
733
+ </div>
734
+ <div class="context-menu-btn">
735
+ <button id="actions-menu-btn">⋯ Actions</button>
736
+ <div class="context-menu" id="context-menu">
737
+ <button class="context-menu-item" id="sync-btn">
738
+ <span>↻</span>
739
+ <span id="sync-btn-text">Sync to Figma</span>
740
+ </button>
741
+ <button class="context-menu-item" id="load-btn">
742
+ <span>↓</span>
743
+ <span>Load from Figma</span>
744
+ </button>
745
+ <button class="context-menu-item" id="fix-references-btn">
746
+ <span>🔗</span>
747
+ <span>Fix Broken References</span>
748
+ </button>
749
+ <button class="context-menu-item" id="find-replace-btn">
750
+ <span>🔍</span>
751
+ <span>Find & Replace Variables</span>
752
+ </button>
753
+ <div class="context-menu-divider"></div>
754
+ <button class="context-menu-item" id="import-btn">
755
+ <span>📂</span>
756
+ <span>Import Tokens</span>
757
+ </button>
758
+ <button class="context-menu-item" id="export-btn">
759
+ <span>💾</span>
760
+ <span>Export JSON</span>
761
+ </button>
762
+ </div>
763
+ </div>
764
+ </div>
765
+
766
+ <div class="content">
767
+ <div class="sidebar" id="tokens-view">
768
+ <div class="search-box">
769
+ <input
770
+ type="text"
771
+ id="search-input"
772
+ placeholder="Search tokens..."
773
+ />
774
+ <button class="search-clear" id="search-clear">×</button>
775
+ </div>
776
+ <div class="sidebar-controls">
777
+ <button id="new-token-btn" style="background: var(--figma-color-bg-brand); color: white; font-weight: 600;">+ New Token</button>
778
+ <button id="expand-all-btn">Expand All</button>
779
+ <button id="collapse-all-btn">Collapse All</button>
780
+ </div>
781
+ <div class="token-tree" id="token-tree">
782
+ <div class="loading">No tokens loaded. Click "Import Tokens" to get started.</div>
783
+ </div>
784
+ </div>
785
+
786
+ <div class="files-view" id="files-view">
787
+ <div class="files-list" id="files-list">
788
+ <div class="loading">No files imported yet</div>
789
+ </div>
790
+ </div>
791
+
792
+ <div class="files-view" id="config-view">
793
+ <div class="config-content" id="config-content">
794
+ <div class="loading">No configuration loaded</div>
795
+ </div>
796
+ </div>
797
+
798
+ <div class="editor" id="editor-content">
799
+ <div class="empty-state">
800
+ <h3>Welcome to Figma Token Sync</h3>
801
+ <ol style="text-align: left; max-width: 450px; margin: 12px auto; line-height: 1.6;">
802
+ <li>Click <strong>"📂 Import Tokens"</strong></li>
803
+ <li>Select your <code>tokens</code> folder from your design system</li>
804
+ <li>All .tokens.json files will be automatically loaded and merged</li>
805
+ </ol>
806
+ <p>
807
+ <button id="get-started-btn" class="primary" onclick="handleImport()">Import Tokens</button>
808
+ </p>
809
+ </div>
810
+ </div>
811
+ </div>
812
+
813
+ <div class="status-bar" id="status-bar">Ready</div>
814
+
815
+ <!-- Find & Replace Variables Modal -->
816
+ <div id="find-replace-modal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;">
817
+ <div style="background: var(--figma-color-bg); border-radius: 8px; padding: 20px; max-width: 450px; width: 90%; box-shadow: 0 4px 12px rgba(0,0,0,0.3); max-height: 80vh; overflow-y: auto;">
818
+ <h2 style="margin: 0 0 16px 0; font-size: 14px; font-weight: 600;">Find & Replace Variables</h2>
819
+
820
+ <div style="margin-bottom: 16px;">
821
+ <label style="display: flex; align-items: center; font-size: 11px; cursor: pointer; margin-bottom: 12px;">
822
+ <input type="checkbox" id="use-regex-pattern" style="margin-right: 6px;">
823
+ Use regex pattern matching
824
+ </label>
825
+ </div>
826
+
827
+ <div id="manual-mode" style="display: block;">
828
+ <div style="margin-bottom: 16px;">
829
+ <label style="display: block; margin-bottom: 6px; font-size: 11px; font-weight: 500;">Find Variable:</label>
830
+ <select id="search-variable-select" style="width: 100%; padding: 6px 8px; border: 1px solid var(--figma-color-border); border-radius: 4px; background: var(--figma-color-bg); color: var(--figma-color-text); font-size: 11px;">
831
+ <option value="">Select variable to find...</option>
832
+ </select>
833
+ </div>
834
+
835
+ <div style="margin-bottom: 16px;">
836
+ <label style="display: block; margin-bottom: 6px; font-size: 11px; font-weight: 500;">Replace With:</label>
837
+ <select id="replace-variable-select" style="width: 100%; padding: 6px 8px; border: 1px solid var(--figma-color-border); border-radius: 4px; background: var(--figma-color-bg); color: var(--figma-color-text); font-size: 11px;">
838
+ <option value="">Select replacement variable...</option>
839
+ </select>
840
+ </div>
841
+ </div>
842
+
843
+ <div id="regex-mode" style="display: none;">
844
+ <div style="margin-bottom: 16px;">
845
+ <label style="display: block; margin-bottom: 6px; font-size: 11px; font-weight: 500;">Find Pattern (regex):</label>
846
+ <input type="text" id="regex-search-pattern" placeholder="e.g. semantic/info" style="width: 100%; padding: 6px 8px; border: 1px solid var(--figma-color-border); border-radius: 4px; background: var(--figma-color-bg); color: var(--figma-color-text); font-size: 11px; font-family: 'Monaco', 'Menlo', monospace;">
847
+ <div style="margin-top: 4px; font-size: 10px; color: var(--figma-color-text-secondary);">Pattern will match against variable names</div>
848
+ </div>
849
+
850
+ <div style="margin-bottom: 16px;">
851
+ <label style="display: block; margin-bottom: 6px; font-size: 11px; font-weight: 500;">Replace Pattern:</label>
852
+ <input type="text" id="regex-replace-pattern" placeholder="e.g. sentiment/info" style="width: 100%; padding: 6px 8px; border: 1px solid var(--figma-color-border); border-radius: 4px; background: var(--figma-color-bg); color: var(--figma-color-text); font-size: 11px; font-family: 'Monaco', 'Menlo', monospace;">
853
+ <div style="margin-top: 4px; font-size: 10px; color: var(--figma-color-text-secondary);">Use $1, $2 for capture groups</div>
854
+ </div>
855
+
856
+ <div id="regex-preview" style="margin-bottom: 16px; padding: 8px; background: var(--figma-color-bg-secondary); border-radius: 4px; font-size: 10px; max-height: 150px; overflow-y: auto; display: none;">
857
+ <div style="font-weight: 600; margin-bottom: 4px; color: var(--figma-color-text);">Matching variables:</div>
858
+ <div id="regex-preview-list" style="font-family: 'Monaco', 'Menlo', monospace; color: var(--figma-color-text-secondary);"></div>
859
+ </div>
860
+ </div>
861
+
862
+ <div style="margin-bottom: 16px;">
863
+ <label style="display: flex; align-items: center; font-size: 11px; cursor: pointer;">
864
+ <input type="checkbox" id="apply-to-whole-page" style="margin-right: 6px;">
865
+ Apply to whole page (otherwise only selected nodes)
866
+ </label>
867
+ </div>
868
+
869
+ <div style="display: flex; gap: 8px; justify-content: flex-end;">
870
+ <button id="find-replace-cancel-btn" style="padding: 6px 12px; border: 1px solid var(--figma-color-border); border-radius: 4px; background: var(--figma-color-bg); color: var(--figma-color-text); cursor: pointer; font-size: 11px;">Cancel</button>
871
+ <button id="find-replace-execute-btn" class="primary" style="padding: 6px 12px;">Replace</button>
872
+ </div>
873
+ </div>
874
+ </div>
875
+ </div>
876
+
877
+ <script>
878
+ // State management
879
+ let tokens = {};
880
+ let tokenFiles = []; // Store original file structures: { path, name, tokens }
881
+ let config = { modes: [], basePixelSize: 16, collectionName: '@DesignID Tokens' }; // Default empty, will detect from tokens
882
+ let selectedTokenPath = null;
883
+ let expandedNodes = new Set();
884
+ let searchQuery = '';
885
+ let currentView = 'tokens'; // 'tokens', 'files', or 'config'
886
+ let selectedFileIndex = null;
887
+ let configData = null; // Store raw config file content
888
+ let newTokenFormData = null; // Store new token form state when switching views
889
+
890
+ // Utility: Check if value is a token reference
891
+ function isTokenReference(value) {
892
+ if (typeof value !== 'string' || !value.startsWith('{') || !value.endsWith('}')) {
893
+ return false;
894
+ }
895
+ const innerContent = value.slice(1, -1);
896
+ return !innerContent.includes('{') && !innerContent.includes('}') && innerContent.trim() !== '';
897
+ }
898
+
899
+ // Utility: Format token value for display
900
+ function formatTokenValue(value) {
901
+ if (value === null || value === undefined) return '';
902
+ if (typeof value === 'string') return value;
903
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
904
+ return JSON.stringify(value);
905
+ }
906
+
907
+ // Utility: Get display value (show reference or actual value)
908
+ function getDisplayValue(value) {
909
+ if (isTokenReference(value)) {
910
+ return value; // Show the reference as-is
911
+ }
912
+ return formatTokenValue(value);
913
+ }
914
+
915
+ // Detect modes from token data
916
+ function detectModesFromTokens(tokensObj) {
917
+ const modes = new Set();
918
+
919
+ function traverse(obj) {
920
+ if (!obj || typeof obj !== 'object') return;
921
+
922
+ // Check if this is a token with $extensions.$mode
923
+ if (obj.$extensions && obj.$extensions.$mode) {
924
+ Object.keys(obj.$extensions.$mode).forEach(mode => modes.add(mode));
925
+ }
926
+
927
+ // Recursively check nested objects
928
+ Object.values(obj).forEach(value => {
929
+ if (value && typeof value === 'object') {
930
+ traverse(value);
931
+ }
932
+ });
933
+ }
934
+
935
+ traverse(tokensObj);
936
+ return Array.from(modes);
937
+ }
938
+
939
+ // Utility: Resolve token reference to actual value
940
+ function resolveTokenReference(ref, mode = null) {
941
+ if (!isTokenReference(ref)) return ref;
942
+ const path = ref.slice(1, -1); // Remove { and }
943
+ const parts = path.split('.');
944
+ let current = tokens;
945
+ for (const part of parts) {
946
+ if (!current || typeof current !== 'object') return ref;
947
+ current = current[part];
948
+ }
949
+ if (current && typeof current === 'object' && '$value' in current) {
950
+ // If a mode is specified and the token has mode-specific values, use that
951
+ if (mode && current.$extensions && current.$extensions.$mode && current.$extensions.$mode[mode]) {
952
+ const modeValue = current.$extensions.$mode[mode];
953
+ // Recursively resolve if the mode value is also a reference
954
+ return isTokenReference(modeValue) ? resolveTokenReference(modeValue, mode) : modeValue;
955
+ }
956
+ // Otherwise use the default value
957
+ const defaultValue = current.$value;
958
+ // Recursively resolve if the default value is also a reference
959
+ return isTokenReference(defaultValue) ? resolveTokenReference(defaultValue, mode) : defaultValue;
960
+ }
961
+ return ref; // Return original if not found
962
+ }
963
+
964
+ // Get the referenced token object (not just its value)
965
+ function getReferencedToken(ref) {
966
+ if (!isTokenReference(ref)) return null;
967
+ const path = ref.slice(1, -1); // Remove { and }
968
+ const parts = path.split('.');
969
+ let current = tokens;
970
+ for (const part of parts) {
971
+ if (!current || typeof current !== 'object') return null;
972
+ current = current[part];
973
+ }
974
+ if (current && typeof current === 'object' && '$value' in current) {
975
+ return current;
976
+ }
977
+ return null;
978
+ }
979
+
980
+ // Utility: Deep merge token objects
981
+ function deepMergeTokens(target, source) {
982
+ for (const key in source) {
983
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
984
+ if (!target[key]) target[key] = {};
985
+ deepMergeTokens(target[key], source[key]);
986
+ } else {
987
+ target[key] = source[key];
988
+ }
989
+ }
990
+ }
991
+
992
+ // Utility: Remove icon tokens from token object
993
+ function removeIconTokens(obj) {
994
+ if (!obj || typeof obj !== 'object') return;
995
+
996
+ // Check all keys in the object
997
+ for (const key in obj) {
998
+ const value = obj[key];
999
+ if (value && typeof value === 'object') {
1000
+ // Check if this is an icon token (has $type === 'icon')
1001
+ if (value.$type === 'icon') {
1002
+ delete obj[key];
1003
+ } else {
1004
+ // Recurse into nested objects/groups
1005
+ removeIconTokens(value);
1006
+ }
1007
+ }
1008
+ }
1009
+ }
1010
+
1011
+ // Initialize
1012
+ window.onload = () => {
1013
+ setupEventListeners();
1014
+ loadInitialTokens();
1015
+ updateModesIndicator();
1016
+ };
1017
+
1018
+ // Update modes indicator in header
1019
+ function updateModesIndicator() {
1020
+ const indicator = document.getElementById('modes-indicator');
1021
+ if (config.modes && config.modes.length > 0) {
1022
+ // Ensure default is always first and not duplicated
1023
+ const modes = config.modes.includes('default')
1024
+ ? config.modes
1025
+ : ['default', ...config.modes];
1026
+ indicator.textContent = `• ${modes.length} mode${modes.length > 1 ? 's' : ''}: ${modes.join(', ')}`;
1027
+ } else {
1028
+ indicator.textContent = '• 1 mode: default';
1029
+ }
1030
+ }
1031
+
1032
+ // Setup event listeners
1033
+ function setupEventListeners() {
1034
+ // Tab navigation
1035
+ document.getElementById('tokens-tab').addEventListener('click', showTokensView);
1036
+ document.getElementById('files-tab').addEventListener('click', showFilesView);
1037
+ document.getElementById('config-tab').addEventListener('click', showConfigView);
1038
+
1039
+ // Context menu
1040
+ const menuBtn = document.getElementById('actions-menu-btn');
1041
+ const contextMenu = document.getElementById('context-menu');
1042
+
1043
+ menuBtn.addEventListener('click', (e) => {
1044
+ e.stopPropagation();
1045
+ contextMenu.classList.toggle('open');
1046
+ });
1047
+
1048
+ // Close context menu when clicking outside
1049
+ document.addEventListener('click', () => {
1050
+ contextMenu.classList.remove('open');
1051
+ });
1052
+
1053
+ // Menu actions
1054
+ document.getElementById('sync-btn').addEventListener('click', () => {
1055
+ contextMenu.classList.remove('open');
1056
+ handleSync();
1057
+ });
1058
+ document.getElementById('load-btn').addEventListener('click', () => {
1059
+ contextMenu.classList.remove('open');
1060
+ handleLoad();
1061
+ });
1062
+ document.getElementById('fix-references-btn').addEventListener('click', () => {
1063
+ contextMenu.classList.remove('open');
1064
+ handleFixBrokenReferences();
1065
+ });
1066
+ document.getElementById('find-replace-btn').addEventListener('click', () => {
1067
+ contextMenu.classList.remove('open');
1068
+ showFindReplaceModal();
1069
+ });
1070
+ document.getElementById('import-btn').addEventListener('click', () => {
1071
+ contextMenu.classList.remove('open');
1072
+ handleImport();
1073
+ });
1074
+ document.getElementById('export-btn').addEventListener('click', () => {
1075
+ contextMenu.classList.remove('open');
1076
+ handleExport();
1077
+ });
1078
+
1079
+ document.getElementById('new-token-btn').addEventListener('click', showNewTokenEditor);
1080
+ document.getElementById('expand-all-btn').addEventListener('click', expandAll);
1081
+ document.getElementById('collapse-all-btn').addEventListener('click', collapseAll);
1082
+
1083
+ // Find/replace modal event listeners
1084
+ document.getElementById('find-replace-cancel-btn').addEventListener('click', closeFindReplaceModal);
1085
+ document.getElementById('find-replace-execute-btn').addEventListener('click', handleFindReplace);
1086
+ document.getElementById('use-regex-pattern').addEventListener('change', toggleFindReplaceMode);
1087
+ document.getElementById('regex-search-pattern').addEventListener('input', updateRegexPreview);
1088
+ document.getElementById('regex-replace-pattern').addEventListener('input', updateRegexPreview);
1089
+
1090
+ const searchInput = document.getElementById('search-input');
1091
+ searchInput.addEventListener('input', handleSearch);
1092
+
1093
+ document.getElementById('search-clear').addEventListener('click', clearSearch);
1094
+ }
1095
+
1096
+ // Handle messages from plugin code
1097
+ window.onmessage = async (event) => {
1098
+ const msg = event.data.pluginMessage;
1099
+ if (!msg) return;
1100
+
1101
+ console.log('UI received:', msg.type);
1102
+
1103
+ switch (msg.type) {
1104
+ case 'init':
1105
+ setStatus(`Connected to: ${msg.payload.documentName}`);
1106
+ // Auto-trigger import if no tokens are present after init
1107
+ setTimeout(() => {
1108
+ if (Object.keys(tokens).length === 0 && tokenFiles.length === 0) {
1109
+ handleImport();
1110
+ }
1111
+ }, 500);
1112
+ break;
1113
+
1114
+ case 'tokens-loaded':
1115
+ tokens = msg.payload.tokens;
1116
+
1117
+ // Detect modes from loaded tokens
1118
+ const detectedModes = detectModesFromTokens(tokens);
1119
+ if (detectedModes.length > 0) {
1120
+ config.modes = detectedModes;
1121
+ console.log('Detected modes from Figma tokens:', config.modes);
1122
+ updateModesIndicator();
1123
+ }
1124
+
1125
+ renderTokenTree();
1126
+ setStatus('Tokens loaded from Figma', 'success');
1127
+ break;
1128
+
1129
+ case 'sync-progress':
1130
+ setStatus(msg.payload.message);
1131
+ break;
1132
+
1133
+ case 'sync-complete':
1134
+ const result = msg.payload.result;
1135
+ let message = msg.payload.message;
1136
+ if (result && result.removed > 0) {
1137
+ message += ` (${result.removed} obsolete variable${result.removed > 1 ? 's' : ''} removed)`;
1138
+ }
1139
+ setStatus(message, 'success');
1140
+ setSyncButtonState(false);
1141
+ break;
1142
+
1143
+ case 'token-updated':
1144
+ case 'token-added':
1145
+ setStatus(`Token "${msg.payload.path}" saved`, 'success');
1146
+ // Update local state
1147
+ setNestedValue(tokens, msg.payload.path, msg.payload.token);
1148
+ renderTokenTree();
1149
+ break;
1150
+
1151
+ case 'token-deleted':
1152
+ setStatus(`Token "${msg.payload.path}" deleted`, 'success');
1153
+ deleteNestedValue(tokens, msg.payload.path);
1154
+ renderTokenTree();
1155
+ showEmptyState();
1156
+ break;
1157
+
1158
+ case 'tokens-exported':
1159
+ downloadJSON(msg.payload.tokens, 'figma-tokens.json');
1160
+ setStatus('Tokens exported', 'success');
1161
+ break;
1162
+
1163
+ case 'fix-references-complete':
1164
+ setStatus(msg.payload.message, 'success');
1165
+ break;
1166
+
1167
+ case 'available-variables':
1168
+ // Populate the dropdowns with available variables
1169
+ populateVariableDropdowns(msg.payload.variables);
1170
+ // Show the modal
1171
+ document.getElementById('find-replace-modal').classList.add('active');
1172
+ setStatus('');
1173
+ break;
1174
+
1175
+ case 'find-replace-progress':
1176
+ setStatus(msg.payload.message);
1177
+ break;
1178
+
1179
+ case 'find-replace-complete':
1180
+ const replacedCount = msg.payload.replacedCount || 0;
1181
+ const scannedCount = msg.payload.scannedNodes || 0;
1182
+ setStatus(`Replaced ${replacedCount} reference${replacedCount !== 1 ? 's' : ''} in ${scannedCount} node${scannedCount !== 1 ? 's' : ''}`, 'success');
1183
+ break;
1184
+
1185
+ case 'error':
1186
+ setStatus(msg.payload.message, 'error');
1187
+ setSyncButtonState(false);
1188
+ break;
1189
+ }
1190
+ };
1191
+
1192
+ // Load initial tokens
1193
+ function loadInitialTokens() {
1194
+ parent.postMessage({ pluginMessage: { type: 'load-tokens' } }, '*');
1195
+ }
1196
+
1197
+ // Handle sync to Figma
1198
+ function handleSync() {
1199
+ if (Object.keys(tokens).length === 0) {
1200
+ setStatus('No tokens to sync. Import token files first.', 'error');
1201
+ return;
1202
+ }
1203
+
1204
+ setSyncButtonState(true);
1205
+ setStatus('Syncing tokens to Figma...');
1206
+ parent.postMessage({
1207
+ pluginMessage: {
1208
+ type: 'sync-tokens',
1209
+ payload: {
1210
+ tokens,
1211
+ config: {
1212
+ basePixelSize: config.basePixelSize,
1213
+ collectionName: config.collectionName
1214
+ }
1215
+ }
1216
+ }
1217
+ }, '*');
1218
+ }
1219
+
1220
+ // Handle load from Figma
1221
+ function handleLoad() {
1222
+ const container = document.getElementById('token-tree');
1223
+ container.innerHTML = '<div class="loading">Loading tokens from Figma...</div>';
1224
+ setStatus('Loading tokens from Figma...');
1225
+ parent.postMessage({
1226
+ pluginMessage: {
1227
+ type: 'load-tokens',
1228
+ payload: {
1229
+ config: {
1230
+ basePixelSize: config.basePixelSize,
1231
+ collectionName: config.collectionName
1232
+ }
1233
+ }
1234
+ }
1235
+ }, '*');
1236
+ }
1237
+
1238
+ // Handle fix broken references
1239
+ function handleFixBrokenReferences() {
1240
+ if (Object.keys(tokens).length === 0) {
1241
+ setStatus('No tokens loaded. Import tokens first.', 'error');
1242
+ return;
1243
+ }
1244
+
1245
+ setStatus('Scanning for broken references...');
1246
+ parent.postMessage({
1247
+ pluginMessage: {
1248
+ type: 'fix-broken-references',
1249
+ payload: {
1250
+ tokens,
1251
+ config: {
1252
+ basePixelSize: config.basePixelSize,
1253
+ collectionName: config.collectionName
1254
+ }
1255
+ }
1256
+ }
1257
+ }, '*');
1258
+ }
1259
+
1260
+ // Show find/replace modal
1261
+ function showFindReplaceModal() {
1262
+ setStatus('Loading variables...');
1263
+ // Request available variables from the plugin
1264
+ parent.postMessage({
1265
+ pluginMessage: {
1266
+ type: 'get-available-variables'
1267
+ }
1268
+ }, '*');
1269
+ }
1270
+
1271
+ // Store available variables for regex preview
1272
+ let availableVariables = [];
1273
+
1274
+ // Handle find and replace variables
1275
+ function handleFindReplace() {
1276
+ const useRegex = document.getElementById('use-regex-pattern').checked;
1277
+ const applyToWholePage = document.getElementById('apply-to-whole-page').checked;
1278
+
1279
+ if (useRegex) {
1280
+ // Regex mode
1281
+ const searchPattern = document.getElementById('regex-search-pattern').value;
1282
+ const replacePattern = document.getElementById('regex-replace-pattern').value;
1283
+
1284
+ if (!searchPattern) {
1285
+ setStatus('Please enter a search pattern', 'error');
1286
+ return;
1287
+ }
1288
+
1289
+ if (!replacePattern) {
1290
+ setStatus('Please enter a replacement pattern', 'error');
1291
+ return;
1292
+ }
1293
+
1294
+ // Close modal
1295
+ document.getElementById('find-replace-modal').classList.remove('active');
1296
+
1297
+ // Get library collection name from config if available
1298
+ const libraryCollectionName = config.collectionName || null;
1299
+ console.log('[UI] Sending find-replace-pattern with library collection:', libraryCollectionName);
1300
+
1301
+ setStatus('Replacing variables with pattern...');
1302
+ parent.postMessage({
1303
+ pluginMessage: {
1304
+ type: 'find-replace-pattern',
1305
+ payload: {
1306
+ searchPattern,
1307
+ replacePattern,
1308
+ applyToWholePage,
1309
+ libraryCollectionName
1310
+ }
1311
+ }
1312
+ }, '*');
1313
+ } else {
1314
+ // Manual mode
1315
+ const searchSelect = document.getElementById('search-variable-select');
1316
+ const replaceSelect = document.getElementById('replace-variable-select');
1317
+
1318
+ const searchVariableId = searchSelect.value;
1319
+ const replaceVariableId = replaceSelect.value;
1320
+
1321
+ if (!searchVariableId) {
1322
+ setStatus('Please select a variable to search for', 'error');
1323
+ return;
1324
+ }
1325
+
1326
+ if (!replaceVariableId) {
1327
+ setStatus('Please select a replacement variable', 'error');
1328
+ return;
1329
+ }
1330
+
1331
+ if (searchVariableId === replaceVariableId) {
1332
+ setStatus('Search and replace variables must be different', 'error');
1333
+ return;
1334
+ }
1335
+
1336
+ // Close modal
1337
+ document.getElementById('find-replace-modal').classList.remove('active');
1338
+
1339
+ setStatus('Replacing variable references...');
1340
+ parent.postMessage({
1341
+ pluginMessage: {
1342
+ type: 'find-replace-variables',
1343
+ payload: {
1344
+ searchVariableId,
1345
+ replaceVariableId,
1346
+ applyToWholePage
1347
+ }
1348
+ }
1349
+ }, '*');
1350
+ }
1351
+ }
1352
+
1353
+ // Close find/replace modal
1354
+ function closeFindReplaceModal() {
1355
+ document.getElementById('find-replace-modal').classList.remove('active');
1356
+ }
1357
+
1358
+ // Toggle between manual and regex mode
1359
+ function toggleFindReplaceMode() {
1360
+ const useRegex = document.getElementById('use-regex-pattern').checked;
1361
+ document.getElementById('manual-mode').style.display = useRegex ? 'none' : 'block';
1362
+ document.getElementById('regex-mode').style.display = useRegex ? 'block' : 'none';
1363
+
1364
+ if (useRegex) {
1365
+ updateRegexPreview();
1366
+ }
1367
+ }
1368
+
1369
+ // Update regex preview
1370
+ function updateRegexPreview() {
1371
+ const searchPattern = document.getElementById('regex-search-pattern').value;
1372
+ const replacePattern = document.getElementById('regex-replace-pattern').value;
1373
+ const previewContainer = document.getElementById('regex-preview');
1374
+ const previewList = document.getElementById('regex-preview-list');
1375
+
1376
+ if (!searchPattern || availableVariables.length === 0) {
1377
+ previewContainer.style.display = 'none';
1378
+ return;
1379
+ }
1380
+
1381
+ try {
1382
+ const regex = new RegExp(searchPattern);
1383
+ const matches = availableVariables.filter(v => regex.test(v.name));
1384
+
1385
+ if (matches.length === 0) {
1386
+ previewList.innerHTML = '<div style="color: var(--figma-color-text-tertiary);">No matches found</div>';
1387
+ previewContainer.style.display = 'block';
1388
+ return;
1389
+ }
1390
+
1391
+ const preview = matches.slice(0, 20).map(v => {
1392
+ const newName = replacePattern ? v.name.replace(regex, replacePattern) : v.name;
1393
+ return '<div style="margin-bottom: 2px;">' +
1394
+ '<span style="color: #f24822;">' + v.name + '</span>' +
1395
+ ' → ' +
1396
+ '<span style="color: #14ae5c;">' + newName + '</span>' +
1397
+ '</div>';
1398
+ }).join('');
1399
+
1400
+ const more = matches.length > 20 ? '<div style="margin-top: 4px; color: var(--figma-color-text-tertiary);">... and ' + (matches.length - 20) + ' more</div>' : '';
1401
+
1402
+ previewList.innerHTML = preview + more;
1403
+ previewContainer.style.display = 'block';
1404
+ } catch (e) {
1405
+ previewList.innerHTML = '<div style="color: #f24822;">Invalid regex pattern</div>';
1406
+ previewContainer.style.display = 'block';
1407
+ }
1408
+ }
1409
+
1410
+ // Populate variable dropdowns
1411
+ function populateVariableDropdowns(variables) {
1412
+ availableVariables = variables;
1413
+ const searchSelect = document.getElementById('search-variable-select');
1414
+ const replaceSelect = document.getElementById('replace-variable-select');
1415
+
1416
+ // Clear existing options
1417
+ searchSelect.innerHTML = '<option value="">Select variable to find...</option>';
1418
+ replaceSelect.innerHTML = '<option value="">Select replacement variable...</option>';
1419
+
1420
+ if (!variables || variables.length === 0) {
1421
+ searchSelect.innerHTML += '<option disabled>No variables found in this file</option>';
1422
+ replaceSelect.innerHTML += '<option disabled>No variables found in this file</option>';
1423
+ return;
1424
+ }
1425
+
1426
+ // Group variables by collection
1427
+ const variablesByCollection = {};
1428
+ variables.forEach(variable => {
1429
+ if (!variablesByCollection[variable.collectionName]) {
1430
+ variablesByCollection[variable.collectionName] = [];
1431
+ }
1432
+ variablesByCollection[variable.collectionName].push(variable);
1433
+ });
1434
+
1435
+ // Add options grouped by collection
1436
+ Object.entries(variablesByCollection).forEach(([collectionName, vars]) => {
1437
+ // Add collection group
1438
+ const searchGroup = document.createElement('optgroup');
1439
+ searchGroup.label = collectionName;
1440
+ const replaceGroup = document.createElement('optgroup');
1441
+ replaceGroup.label = collectionName;
1442
+
1443
+ // Sort variables by name
1444
+ vars.sort((a, b) => a.name.localeCompare(b.name));
1445
+
1446
+ // Add each variable
1447
+ vars.forEach(variable => {
1448
+ // Format display text with type info
1449
+ const typeLabel = variable.resolvedType ? ` (${variable.resolvedType.toLowerCase()})` : '';
1450
+ const aliasLabel = variable.isAlias ? ' 🔗' : '';
1451
+ const libraryLabel = variable.isLibrary ? ' 📚' : '';
1452
+ const displayText = `${variable.name}${typeLabel}${aliasLabel}${libraryLabel}`;
1453
+
1454
+ const searchOption = document.createElement('option');
1455
+ searchOption.value = variable.id;
1456
+ searchOption.textContent = displayText;
1457
+ searchGroup.appendChild(searchOption);
1458
+
1459
+ const replaceOption = document.createElement('option');
1460
+ replaceOption.value = variable.id;
1461
+ const replaceDisplayText = `${variable.name}${typeLabel}${aliasLabel}${libraryLabel}`;
1462
+ replaceOption.textContent = replaceDisplayText;
1463
+ replaceGroup.appendChild(replaceOption);
1464
+ });
1465
+
1466
+ searchSelect.appendChild(searchGroup);
1467
+ replaceSelect.appendChild(replaceGroup);
1468
+ });
1469
+ }
1470
+
1471
+ // Handle import tokens
1472
+ function handleImport() {
1473
+ const input = document.createElement('input');
1474
+ input.type = 'file';
1475
+ input.accept = '.json,.tokens.json,.ts,.mjs,.js';
1476
+ input.multiple = true;
1477
+ input.setAttribute('webkitdirectory', '');
1478
+ input.setAttribute('directory', '');
1479
+ input.onchange = async (e) => {
1480
+ const files = e.target.files;
1481
+ if (!files || files.length === 0) return;
1482
+
1483
+ // Look for config file first
1484
+ const configFile = Array.from(files).find(file =>
1485
+ file.name === 'designid.config.ts' || file.name === 'designid.config.mjs' || file.name === 'designid.config.js'
1486
+ );
1487
+
1488
+ // Filter for .tokens.json files only, excluding icon files (svg.*.tokens.json)
1489
+ const jsonFiles = Array.from(files).filter(file =>
1490
+ file.name.endsWith('.tokens.json') && !file.name.startsWith('svg.')
1491
+ );
1492
+
1493
+ if (jsonFiles.length === 0) {
1494
+ setStatus('No .tokens.json files found in the selected directory.', 'error');
1495
+ return;
1496
+ }
1497
+
1498
+ // Read config file first if found
1499
+ if (configFile) {
1500
+ setStatus('Reading config file...');
1501
+ try {
1502
+ const configText = await readFileAsText(configFile);
1503
+ configData = { fileName: configFile.name, content: configText }; // Store for config view
1504
+
1505
+ // Extract $name from config for collection name
1506
+ const nameMatch = configText.match(/\$name\s*:\s*['"]([^'"]+)['"]/);
1507
+ if (nameMatch) {
1508
+ config.collectionName = nameMatch[1];
1509
+ console.log('Found collection name in config:', config.collectionName);
1510
+ }
1511
+
1512
+ // Extract $modes from config using regex
1513
+ const modesMatch = configText.match(/\$modes\s*:\s*\{([^}]+)\}/);
1514
+ if (modesMatch) {
1515
+ const modesStr = modesMatch[1];
1516
+ // Extract mode names (keys in the object) - keep all modes including default
1517
+ const modeNames = [];
1518
+ const keyMatches = modesStr.matchAll(/['"]?([\w-]+)['"]?\s*:/g);
1519
+ for (const match of keyMatches) {
1520
+ const modeName = match[1];
1521
+ modeNames.push(modeName);
1522
+ }
1523
+ config.modes = modeNames;
1524
+ console.log('Found modes in config:', config.modes);
1525
+ const totalModes = config.modes.length;
1526
+ setStatus(`✓ Config loaded with ${totalModes} mode${totalModes > 1 ? 's' : ''}: ${config.modes.join(', ')}`);
1527
+ updateModesIndicator();
1528
+ }
1529
+ } catch (error) {
1530
+ console.warn('Failed to parse config file:', error);
1531
+ setStatus('Config file found but could not be parsed', 'warning');
1532
+ }
1533
+ }
1534
+
1535
+ // Now read token files
1536
+ setStatus(`Processing ${jsonFiles.length} token file${jsonFiles.length > 1 ? 's' : ''}...`);
1537
+
1538
+ let importedTokens = {};
1539
+ let tokenFilesFound = 0;
1540
+ tokenFiles = []; // Reset file tracking
1541
+
1542
+ for (const file of jsonFiles) {
1543
+ try {
1544
+ const text = await readFileAsText(file);
1545
+ const fileTokens = JSON.parse(text);
1546
+
1547
+ // Store file structure for later export
1548
+ tokenFiles.push({
1549
+ path: file.webkitRelativePath || file.name,
1550
+ name: file.name,
1551
+ tokens: JSON.parse(JSON.stringify(fileTokens)) // Deep clone
1552
+ });
1553
+
1554
+ deepMergeTokens(importedTokens, fileTokens);
1555
+ tokenFilesFound++;
1556
+ } catch (error) {
1557
+ console.warn(`Failed to import ${file.name}:`, error);
1558
+ }
1559
+ }
1560
+
1561
+ if (tokenFilesFound > 0) {
1562
+ // Remove icon tokens from imported data
1563
+ removeIconTokens(importedTokens);
1564
+
1565
+ tokens = importedTokens;
1566
+
1567
+ // If no config file was found or no modes detected from config, detect from tokens
1568
+ if (!config.modes || config.modes.length === 0) {
1569
+ const detectedModes = detectModesFromTokens(tokens);
1570
+ if (detectedModes.length > 0) {
1571
+ config.modes = detectedModes;
1572
+ console.log('Detected modes from tokens:', config.modes);
1573
+ updateModesIndicator();
1574
+ }
1575
+ }
1576
+
1577
+ renderTokenTree();
1578
+ const modeInfo = config.modes.length > 0 ? ` (${config.modes.length} mode${config.modes.length > 1 ? 's' : ''} available)` : ' (1 mode: default)';
1579
+ setStatus(`✓ Imported ${tokenFilesFound} token file${tokenFilesFound > 1 ? 's' : ''}${modeInfo}`, 'success');
1580
+ } else {
1581
+ setStatus('Failed to import token files', 'error');
1582
+ }
1583
+ };
1584
+ input.click();
1585
+ }
1586
+
1587
+ // Helper to read file as text (promisified)
1588
+ function readFileAsText(file) {
1589
+ return new Promise((resolve, reject) => {
1590
+ const reader = new FileReader();
1591
+ reader.onload = (e) => resolve(e.target.result);
1592
+ reader.onerror = (e) => reject(e);
1593
+ reader.readAsText(file);
1594
+ });
1595
+ }
1596
+
1597
+ // Handle export JSON
1598
+ function handleExport() {
1599
+ if (Object.keys(tokens).length === 0) {
1600
+ setStatus('No tokens to export', 'error');
1601
+ return;
1602
+ }
1603
+
1604
+ // Show export options dialog
1605
+ showExportDialog();
1606
+ }
1607
+
1608
+ // Show export dialog with options
1609
+ function showExportDialog() {
1610
+ const dialog = document.createElement('div');
1611
+ dialog.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000;';
1612
+
1613
+ const content = document.createElement('div');
1614
+ content.style.cssText = 'background: var(--figma-color-bg); padding: 24px; border-radius: 8px; max-width: 400px; width: 90%;';
1615
+
1616
+ content.innerHTML = `
1617
+ <h3 style="margin: 0 0 16px 0;">Export Tokens</h3>
1618
+ <p style="margin: 0 0 16px 0; font-size: 11px; color: var(--figma-color-text-secondary);">
1619
+ Choose how to export your tokens:
1620
+ </p>
1621
+ <div style="display: flex; flex-direction: column; gap: 8px;">
1622
+ <button class="primary" onclick="exportCurrentTokens()" style="width: 100%; padding: 12px;">
1623
+ 💾 Download as Single JSON
1624
+ </button>
1625
+ <button onclick="exportFromFigma()" style="width: 100%; padding: 12px;">
1626
+ ↓ Export from Figma (Latest Values)
1627
+ </button>
1628
+ <button onclick="closeExportDialog()" style="width: 100%; padding: 12px;">
1629
+ Cancel
1630
+ </button>
1631
+ </div>
1632
+ <p style="margin: 16px 0 0 0; font-size: 10px; color: var(--figma-color-text-secondary);">
1633
+ <strong>Single JSON:</strong> Downloads all tokens in one file.<br>
1634
+ <strong>From Figma:</strong> Exports the latest values from Figma variables.
1635
+ </p>
1636
+ `;
1637
+
1638
+ dialog.appendChild(content);
1639
+ dialog.id = 'export-dialog';
1640
+ dialog.onclick = (e) => {
1641
+ if (e.target === dialog) closeExportDialog();
1642
+ };
1643
+ document.body.appendChild(dialog);
1644
+ }
1645
+
1646
+ // Export current tokens in editor
1647
+ function exportCurrentTokens() {
1648
+ downloadJSON(tokens, 'tokens.json');
1649
+ setStatus('Tokens exported', 'success');
1650
+ closeExportDialog();
1651
+ }
1652
+
1653
+ // Update token values recursively - updates fileTokens with values from mergedTokens
1654
+ function updateTokenValues(fileTokens, mergedTokens, path = []) {
1655
+ const result = {};
1656
+
1657
+ for (const key in fileTokens) {
1658
+ if (key.startsWith('$')) {
1659
+ // Copy metadata as-is
1660
+ result[key] = fileTokens[key];
1661
+ continue;
1662
+ }
1663
+
1664
+ const value = fileTokens[key];
1665
+
1666
+ // Check if this token exists in merged tokens
1667
+ let current = mergedTokens;
1668
+ const fullPath = [...path, key];
1669
+ for (const part of fullPath) {
1670
+ if (!current || typeof current !== 'object') break;
1671
+ current = current[part];
1672
+ }
1673
+
1674
+ if (value && typeof value === 'object') {
1675
+ if ('$type' in value && '$value' in value) {
1676
+ // This is a token - update it with current value
1677
+ if (current && typeof current === 'object' && '$type' in current && '$value' in current) {
1678
+ result[key] = { ...current };
1679
+ } else {
1680
+ result[key] = value; // Keep original if not found in merged
1681
+ }
1682
+ } else {
1683
+ // This is a group - recurse
1684
+ result[key] = updateTokenValues(value, mergedTokens, fullPath);
1685
+ }
1686
+ } else {
1687
+ result[key] = value;
1688
+ }
1689
+ }
1690
+
1691
+ return result;
1692
+ }
1693
+
1694
+ // Export from Figma
1695
+ function exportFromFigma() {
1696
+ parent.postMessage({ pluginMessage: { type: 'export-tokens' } }, '*');
1697
+ closeExportDialog();
1698
+ }
1699
+
1700
+ // Close export dialog
1701
+ function closeExportDialog() {
1702
+ const dialog = document.getElementById('export-dialog');
1703
+ if (dialog) dialog.remove();
1704
+ }
1705
+
1706
+ // Download JSON file
1707
+ function downloadJSON(data, filename) {
1708
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
1709
+ const url = URL.createObjectURL(blob);
1710
+ const a = document.createElement('a');
1711
+ a.href = url;
1712
+ a.download = filename;
1713
+ a.click();
1714
+ URL.revokeObjectURL(url);
1715
+ }
1716
+
1717
+ // Set sync button state
1718
+ function setSyncButtonState(loading) {
1719
+ const btn = document.getElementById('sync-btn');
1720
+ const text = document.getElementById('sync-btn-text');
1721
+ btn.disabled = loading;
1722
+ text.textContent = loading ? '⏳ Syncing...' : 'Sync to Figma';
1723
+ }
1724
+
1725
+ // Set status message
1726
+ function setStatus(message, type = '') {
1727
+ const statusBar = document.getElementById('status-bar');
1728
+ statusBar.textContent = message;
1729
+ statusBar.className = 'status-bar' + (type ? ' ' + type : '');
1730
+
1731
+ if (type) {
1732
+ setTimeout(() => {
1733
+ statusBar.className = 'status-bar';
1734
+ statusBar.textContent = 'Ready';
1735
+ }, 3000);
1736
+ }
1737
+ }
1738
+
1739
+ // Render token tree
1740
+ function renderTokenTree() {
1741
+ const container = document.getElementById('token-tree');
1742
+ container.innerHTML = '';
1743
+
1744
+ if (Object.keys(tokens).length === 0) {
1745
+ container.innerHTML = '<div class="loading">No tokens loaded. Click "Import Tokens" to get started.</div>';
1746
+ return;
1747
+ }
1748
+
1749
+ const filtered = searchQuery ? filterTokens(tokens, searchQuery) : tokens;
1750
+ const tree = buildTree(filtered);
1751
+ container.appendChild(tree);
1752
+ }
1753
+
1754
+ // Build tree DOM
1755
+ function buildTree(obj, path = '') {
1756
+ const container = document.createElement('div');
1757
+
1758
+ for (const [key, value] of Object.entries(obj)) {
1759
+ if (key.startsWith('$')) continue;
1760
+
1761
+ const currentPath = path ? `${path}.${key}` : key;
1762
+ const isToken = value && typeof value === 'object' && '$type' in value && '$value' in value;
1763
+ const hasChildren = !isToken && typeof value === 'object';
1764
+
1765
+ const node = document.createElement('div');
1766
+ node.className = 'tree-node';
1767
+
1768
+ const header = document.createElement('div');
1769
+ header.className = 'tree-node-header';
1770
+ if (selectedTokenPath === currentPath) {
1771
+ header.classList.add('selected');
1772
+ }
1773
+
1774
+ const icon = document.createElement('span');
1775
+ icon.className = 'tree-node-icon';
1776
+ icon.textContent = isToken ? '●' : (expandedNodes.has(currentPath) ? '▼' : '▶');
1777
+
1778
+ const label = document.createElement('span');
1779
+ label.className = 'tree-node-label';
1780
+ label.textContent = key;
1781
+
1782
+ header.appendChild(icon);
1783
+ header.appendChild(label);
1784
+
1785
+ if (isToken) {
1786
+ const type = document.createElement('span');
1787
+ type.className = 'tree-node-type';
1788
+ type.textContent = value.$type;
1789
+ header.appendChild(type);
1790
+
1791
+ // Add value preview
1792
+ const preview = document.createElement('span');
1793
+ preview.className = 'tree-node-value';
1794
+
1795
+ if (value.$type === 'color') {
1796
+ // Show color swatches for color tokens
1797
+ const swatchGroup = document.createElement('div');
1798
+ swatchGroup.className = 'color-swatch-group';
1799
+
1800
+ // Default/light swatch
1801
+ const defaultSwatch = document.createElement('span');
1802
+ defaultSwatch.className = 'color-swatch';
1803
+ const resolvedColor = isTokenReference(value.$value) ? resolveTokenReference(value.$value) : value.$value;
1804
+ defaultSwatch.style.backgroundColor = resolvedColor;
1805
+ defaultSwatch.title = `light: ${getDisplayValue(value.$value)}`;
1806
+ swatchGroup.appendChild(defaultSwatch);
1807
+
1808
+ // Collect all mode names from both the current token and referenced token
1809
+ const allModes = new Set();
1810
+
1811
+ // Add modes from current token
1812
+ if (value.$extensions && value.$extensions.$mode) {
1813
+ Object.keys(value.$extensions.$mode).forEach(mode => allModes.add(mode));
1814
+ }
1815
+
1816
+ // If the value is a reference, also check the referenced token's modes
1817
+ if (isTokenReference(value.$value)) {
1818
+ const referencedToken = getReferencedToken(value.$value);
1819
+ if (referencedToken && referencedToken.$extensions && referencedToken.$extensions.$mode) {
1820
+ Object.keys(referencedToken.$extensions.$mode).forEach(mode => allModes.add(mode));
1821
+ }
1822
+ }
1823
+
1824
+ // Show swatches for all modes
1825
+ if (allModes.size > 0) {
1826
+ for (const modeName of Array.from(allModes)) {
1827
+ const modeSwatch = document.createElement('span');
1828
+ modeSwatch.className = 'color-swatch mode-swatch';
1829
+
1830
+ // Try to get mode value from current token first
1831
+ let modeValue = value.$extensions && value.$extensions.$mode && value.$extensions.$mode[modeName];
1832
+
1833
+ // If not found and value is a reference, get it from the referenced token
1834
+ if (!modeValue && isTokenReference(value.$value)) {
1835
+ modeValue = value.$value; // Use the reference, it will resolve with mode
1836
+ }
1837
+
1838
+ const modeColor = isTokenReference(modeValue) ? resolveTokenReference(modeValue, modeName) : modeValue;
1839
+ modeSwatch.style.backgroundColor = modeColor;
1840
+ modeSwatch.setAttribute('data-mode', modeName);
1841
+ modeSwatch.title = `${modeName}: ${modeValue ? getDisplayValue(modeValue) : 'from reference'}`;
1842
+ swatchGroup.appendChild(modeSwatch);
1843
+ }
1844
+ }
1845
+
1846
+ preview.appendChild(swatchGroup);
1847
+ } else {
1848
+ // Show text for non-color tokens
1849
+ const displayValue = getDisplayValue(value.$value);
1850
+ let previewText = displayValue.length > 30 ? displayValue.substring(0, 27) + '...' : displayValue;
1851
+
1852
+ // Check if there are mode-specific values
1853
+ if (value.$extensions && value.$extensions.$mode) {
1854
+ const modeValues = value.$extensions.$mode;
1855
+ const modeNames = Object.keys(modeValues);
1856
+ if (modeNames.length > 0) {
1857
+ // Add mode indicators
1858
+ const modeIndicators = modeNames.map(modeName => {
1859
+ const modeValue = getDisplayValue(modeValues[modeName]);
1860
+ const shortModeValue = modeValue.length > 20 ? modeValue.substring(0, 17) + '...' : modeValue;
1861
+ return `${modeName}: ${shortModeValue}`;
1862
+ }).join(', ');
1863
+ previewText = `${previewText} | ${modeIndicators}`;
1864
+ }
1865
+ }
1866
+
1867
+ preview.textContent = previewText;
1868
+ if (isTokenReference(value.$value)) {
1869
+ preview.style.color = '#0066ff';
1870
+ preview.style.fontStyle = 'italic';
1871
+ }
1872
+ }
1873
+
1874
+ header.appendChild(preview);
1875
+
1876
+ header.addEventListener('click', () => selectToken(currentPath, value));
1877
+ } else if (hasChildren) {
1878
+ header.addEventListener('click', () => toggleNode(currentPath));
1879
+ }
1880
+
1881
+ node.appendChild(header);
1882
+
1883
+ if (hasChildren) {
1884
+ const children = document.createElement('div');
1885
+ children.className = 'tree-node-children';
1886
+ if (!expandedNodes.has(currentPath)) {
1887
+ children.classList.add('collapsed');
1888
+ }
1889
+ children.appendChild(buildTree(value, currentPath));
1890
+ node.appendChild(children);
1891
+ }
1892
+
1893
+ container.appendChild(node);
1894
+ }
1895
+
1896
+ return container;
1897
+ }
1898
+
1899
+ // Toggle node expansion
1900
+ function toggleNode(path) {
1901
+ if (expandedNodes.has(path)) {
1902
+ expandedNodes.delete(path);
1903
+ } else {
1904
+ expandedNodes.add(path);
1905
+ }
1906
+ renderTokenTree();
1907
+ }
1908
+
1909
+ // Select token
1910
+ function selectToken(path, token) {
1911
+ selectedTokenPath = path;
1912
+ renderTokenTree();
1913
+ showTokenEditor(path, token);
1914
+ }
1915
+
1916
+ // Navigate to a token by path
1917
+ function navigateToToken(path) {
1918
+ // Expand all parent nodes in the path
1919
+ const parts = path.split('.');
1920
+ for (let i = 0; i < parts.length; i++) {
1921
+ const parentPath = parts.slice(0, i + 1).join('.');
1922
+ expandedNodes.add(parentPath);
1923
+ }
1924
+
1925
+ // Get the token object
1926
+ const parts2 = path.split('.');
1927
+ let token = tokens;
1928
+ for (const part of parts2) {
1929
+ if (!token || typeof token !== 'object') return;
1930
+ token = token[part];
1931
+ }
1932
+
1933
+ if (token && typeof token === 'object' && '$type' in token && '$value' in token) {
1934
+ selectToken(path, token);
1935
+ }
1936
+ }
1937
+
1938
+ // Show new token editor
1939
+ function showNewTokenEditor(savedFormData = null) {
1940
+ selectedTokenPath = null;
1941
+ const editor = document.getElementById('editor-content');
1942
+
1943
+ // Build file options from tokenFiles
1944
+ const fileOptions = tokenFiles.length > 0
1945
+ ? tokenFiles.map((file, index) =>
1946
+ `<option value="${index}">${file.name || file.path}</option>`
1947
+ ).join('')
1948
+ : '';
1949
+
1950
+ const form = `
1951
+ <div class="editor-form">
1952
+ <h3>Create New Token</h3>
1953
+ <div class="form-group">
1954
+ <label>Token Path *</label>
1955
+ <input type="text" id="token-path" placeholder="e.g., color.primary.base or spacing.md" />
1956
+ <small style="color: var(--figma-color-text-secondary); display: block; margin-top: 4px;">
1957
+ Use dot notation to create nested groups (e.g., color.primary.base)
1958
+ </small>
1959
+ </div>
1960
+ <div class="form-group">
1961
+ <label>Type *</label>
1962
+ <select id="token-type">
1963
+ <option value="color">color</option>
1964
+ <option value="dimension">dimension</option>
1965
+ <option value="fontFamily">fontFamily</option>
1966
+ <option value="fontWeight">fontWeight</option>
1967
+ <option value="fontSize">fontSize</option>
1968
+ <option value="lineHeight">lineHeight</option>
1969
+ <option value="letterSpacing">letterSpacing</option>
1970
+ <option value="number">number</option>
1971
+ <option value="string">string</option>
1972
+ <option value="duration">duration</option>
1973
+ <option value="shadow">shadow</option>
1974
+ <option value="border">border</option>
1975
+ <option value="typography">typography</option>
1976
+ </select>
1977
+ </div>
1978
+ <div class="form-group">
1979
+ <label>Value (default / light) *</label>
1980
+ <input type="text" id="token-value" placeholder="e.g., #FF0000 or {color.red.500}" />
1981
+ <small style="color: var(--figma-color-text-secondary); display: block; margin-top: 4px;">
1982
+ Use {token.path} syntax to reference other tokens
1983
+ </small>
1984
+ </div>
1985
+ ${config.modes && config.modes.length > 0 ? config.modes.filter(mode => mode !== 'default').map(mode => `
1986
+ <div class="form-group">
1987
+ <label>Value (${mode} mode)</label>
1988
+ <input type="text" id="token-value-${mode}" placeholder="Leave empty to use default" />
1989
+ </div>
1990
+ `).join('') : ''}
1991
+ <div class="form-group">
1992
+ <label>Target File *</label>
1993
+ <select id="token-file">
1994
+ ${fileOptions}
1995
+ <option value="new" ${tokenFiles.length === 0 ? 'selected' : ''}>+ Create New File</option>
1996
+ </select>
1997
+ </div>
1998
+ <div class="form-group" id="new-file-group" style="display: ${tokenFiles.length === 0 ? 'block' : 'none'};">
1999
+ <label>New File Name *</label>
2000
+ <input type="text" id="new-file-name" placeholder="e.g., colors.tokens.json" />
2001
+ <small style="color: var(--figma-color-text-secondary); display: block; margin-top: 4px;">
2002
+ File name should end with .tokens.json
2003
+ </small>
2004
+ </div>
2005
+ <div class="form-actions">
2006
+ <button id="save-new-token" class="primary">Create Token</button>
2007
+ <button id="cancel-edit">Cancel</button>
2008
+ </div>
2009
+ </div>
2010
+ `;
2011
+
2012
+ editor.innerHTML = form;
2013
+
2014
+ // Restore saved form data if available
2015
+ if (savedFormData) {
2016
+ if (savedFormData.path) document.getElementById('token-path').value = savedFormData.path;
2017
+ if (savedFormData.type) document.getElementById('token-type').value = savedFormData.type;
2018
+ if (savedFormData.value) document.getElementById('token-value').value = savedFormData.value;
2019
+ if (savedFormData.fileIndex) document.getElementById('token-file').value = savedFormData.fileIndex;
2020
+ if (savedFormData.newFileName) document.getElementById('new-file-name').value = savedFormData.newFileName;
2021
+ if (savedFormData.fileIndex === 'new') {
2022
+ document.getElementById('new-file-group').style.display = 'block';
2023
+ }
2024
+ if (savedFormData.modeValues) {
2025
+ Object.entries(savedFormData.modeValues).forEach(([mode, val]) => {
2026
+ const input = document.getElementById(`token-value-${mode}`);
2027
+ if (input) input.value = val;
2028
+ });
2029
+ }
2030
+ }
2031
+
2032
+ // Track form changes to persist state
2033
+ const saveFormState = () => {
2034
+ const modeValues = {};
2035
+ if (config.modes && config.modes.length > 0) {
2036
+ config.modes.filter(m => m !== 'default').forEach(mode => {
2037
+ const input = document.getElementById(`token-value-${mode}`);
2038
+ if (input && input.value) modeValues[mode] = input.value;
2039
+ });
2040
+ }
2041
+
2042
+ newTokenFormData = {
2043
+ path: document.getElementById('token-path').value,
2044
+ type: document.getElementById('token-type').value,
2045
+ value: document.getElementById('token-value').value,
2046
+ fileIndex: document.getElementById('token-file').value,
2047
+ newFileName: document.getElementById('new-file-name').value,
2048
+ modeValues
2049
+ };
2050
+ };
2051
+
2052
+ // Add listeners to save state on input
2053
+ document.getElementById('token-path').addEventListener('input', saveFormState);
2054
+ document.getElementById('token-type').addEventListener('change', saveFormState);
2055
+ document.getElementById('token-value').addEventListener('input', saveFormState);
2056
+ document.getElementById('token-file').addEventListener('change', saveFormState);
2057
+ document.getElementById('new-file-name').addEventListener('input', saveFormState);
2058
+ config.modes.filter(m => m !== 'default').forEach(mode => {
2059
+ const input = document.getElementById(`token-value-${mode}`);
2060
+ if (input) input.addEventListener('input', saveFormState);
2061
+ });
2062
+
2063
+ // Handle file selection change
2064
+ const fileSelect = document.getElementById('token-file');
2065
+ const newFileGroup = document.getElementById('new-file-group');
2066
+ fileSelect.addEventListener('change', (e) => {
2067
+ if (e.target.value === 'new') {
2068
+ newFileGroup.style.display = 'block';
2069
+ } else {
2070
+ newFileGroup.style.display = 'none';
2071
+ }
2072
+ });
2073
+
2074
+ // Save button
2075
+ document.getElementById('save-new-token').addEventListener('click', async () => {
2076
+ const path = document.getElementById('token-path').value.trim();
2077
+ const type = document.getElementById('token-type').value;
2078
+ const value = document.getElementById('token-value').value.trim();
2079
+ const fileIndex = document.getElementById('token-file').value;
2080
+
2081
+ if (!path) {
2082
+ alert('Token path is required');
2083
+ return;
2084
+ }
2085
+
2086
+ if (!value) {
2087
+ alert('Token value is required');
2088
+ return;
2089
+ }
2090
+
2091
+ // Validate file selection
2092
+ let targetFileName;
2093
+ if (fileIndex === 'new') {
2094
+ targetFileName = document.getElementById('new-file-name').value.trim();
2095
+ if (!targetFileName) {
2096
+ alert('File name is required when creating a new file');
2097
+ return;
2098
+ }
2099
+ if (!targetFileName.endsWith('.tokens.json')) {
2100
+ targetFileName += '.tokens.json';
2101
+ }
2102
+ } else {
2103
+ if (tokenFiles.length === 0) {
2104
+ alert('No files available. Please specify a new file name.');
2105
+ return;
2106
+ }
2107
+ targetFileName = tokenFiles[parseInt(fileIndex)].name || tokenFiles[parseInt(fileIndex)].path;
2108
+ }
2109
+
2110
+ // Build the new token object
2111
+ const newToken = {
2112
+ $type: type,
2113
+ $value: value
2114
+ };
2115
+
2116
+ // Add mode-specific values if any
2117
+ const modeValues = {};
2118
+ if (config.modes && config.modes.length > 0) {
2119
+ for (const mode of config.modes.filter(m => m !== 'default')) {
2120
+ const modeInput = document.getElementById(`token-value-${mode}`);
2121
+ if (modeInput && modeInput.value.trim()) {
2122
+ modeValues[mode] = modeInput.value.trim();
2123
+ }
2124
+ }
2125
+ }
2126
+
2127
+ if (Object.keys(modeValues).length > 0) {
2128
+ newToken.$extensions = { $mode: modeValues };
2129
+ }
2130
+
2131
+ // Add token to the tokens object using path
2132
+ const pathParts = path.split('.');
2133
+ let current = tokens;
2134
+ for (let i = 0; i < pathParts.length - 1; i++) {
2135
+ const part = pathParts[i];
2136
+ if (!current[part]) {
2137
+ current[part] = {};
2138
+ }
2139
+ current = current[part];
2140
+ }
2141
+ current[pathParts[pathParts.length - 1]] = newToken;
2142
+
2143
+ // Update or create the file entry
2144
+ if (fileIndex === 'new') {
2145
+ // Create a new file entry
2146
+ const newFileTokens = {};
2147
+ let curr = newFileTokens;
2148
+ for (let i = 0; i < pathParts.length - 1; i++) {
2149
+ curr[pathParts[i]] = {};
2150
+ curr = curr[pathParts[i]];
2151
+ }
2152
+ curr[pathParts[pathParts.length - 1]] = newToken;
2153
+
2154
+ tokenFiles.push({
2155
+ name: targetFileName,
2156
+ path: targetFileName,
2157
+ tokens: newFileTokens
2158
+ });
2159
+ } else {
2160
+ // Add to existing file
2161
+ const targetFile = tokenFiles[parseInt(fileIndex)];
2162
+ let curr = targetFile.tokens;
2163
+ for (let i = 0; i < pathParts.length - 1; i++) {
2164
+ const part = pathParts[i];
2165
+ if (!curr[part]) {
2166
+ curr[part] = {};
2167
+ }
2168
+ curr = curr[part];
2169
+ }
2170
+ curr[pathParts[pathParts.length - 1]] = newToken;
2171
+ }
2172
+
2173
+ // Re-render everything
2174
+ renderTokenTree();
2175
+ renderFilesView();
2176
+
2177
+ // Clear the saved form data
2178
+ newTokenFormData = null;
2179
+
2180
+ // Select and navigate to edit the new token
2181
+ selectedTokenPath = path;
2182
+ expandAllParents(path);
2183
+ selectToken(path, newToken);
2184
+
2185
+ setStatus(`Token "${path}" created in ${targetFileName}`, 'success');
2186
+ });
2187
+
2188
+ // Cancel button
2189
+ document.getElementById('cancel-edit').addEventListener('click', () => {
2190
+ newTokenFormData = null;
2191
+ editor.innerHTML = '<div class="empty-state"><h3>Welcome to Figma Token Sync</h3><p>Select a token from the sidebar to edit, or click "+ New Token" to create one.</p></div>';
2192
+ });
2193
+ }
2194
+
2195
+ // Helper function to expand all parent nodes
2196
+ function expandAllParents(path) {
2197
+ const parts = path.split('.');
2198
+ for (let i = 0; i < parts.length; i++) {
2199
+ const parentPath = parts.slice(0, i + 1).join('.');
2200
+ expandedNodes.add(parentPath);
2201
+ }
2202
+ renderTokenTree();
2203
+ }
2204
+
2205
+ // Show token editor
2206
+ function showTokenEditor(path, token) {
2207
+ const editor = document.getElementById('editor-content');
2208
+
2209
+ console.log('showTokenEditor - config.modes:', config.modes);
2210
+ console.log('showTokenEditor - token.$extensions:', token.$extensions);
2211
+
2212
+ const form = `
2213
+ <div class="editor-form">
2214
+ <h3>Edit Token</h3>
2215
+ <div class="form-group">
2216
+ <label>Path</label>
2217
+ <input type="text" id="token-path" value="${path}" readonly />
2218
+ </div>
2219
+ <div class="form-group">
2220
+ <label>Type</label>
2221
+ <select id="token-type">
2222
+ ${['color', 'dimension', 'fontFamily', 'fontWeight', 'fontSize', 'lineHeight', 'letterSpacing', 'number', 'string', 'duration', 'shadow', 'border', 'typography'].map(t =>
2223
+ `<option value="${t}" ${token.$type === t ? 'selected' : ''}>${t}</option>`
2224
+ ).join('')}
2225
+ </select>
2226
+ </div>
2227
+ <div class="form-group">
2228
+ <label>Value (default / light)</label>
2229
+ <input type="text" id="token-value" value="${formatTokenValue(token.$value).replace(/"/g, '&quot;')}" />
2230
+ ${isTokenReference(token.$value) ? `
2231
+ <div class="token-reference-info" style="cursor: pointer;" onclick="navigateToToken('${token.$value.slice(1, -1)}')">
2232
+ <small style="color: #0066ff;">📎 Token Reference - Click to navigate</small>
2233
+ </div>
2234
+ ` : ''}
2235
+ </div>
2236
+ ${config.modes && config.modes.length > 0 ? config.modes.filter(mode => mode !== 'default').map(mode => {
2237
+ const modeValue = token.$extensions?.$mode?.[mode] || '';
2238
+ console.log(`Mode ${mode} value:`, modeValue);
2239
+ return `
2240
+ <div class="form-group">
2241
+ <label>Value (${mode} mode)</label>
2242
+ <input type="text" id="token-value-${mode}" value="${modeValue ? formatTokenValue(modeValue).replace(/"/g, '&quot;') : ''}" placeholder="Leave empty to use default" />
2243
+ ${isTokenReference(modeValue) ? `
2244
+ <div class="token-reference-info" style="cursor: pointer;" onclick="navigateToToken('${modeValue.slice(1, -1)}')">
2245
+ <small style="color: #0066ff;">📎 Token Reference - Click to navigate</small>
2246
+ </div>
2247
+ ` : ''}
2248
+ </div>
2249
+ `;
2250
+ }).join('') : '<div class="form-group"><small style="color: #999;">No modes configured. Import tokens with designid.config.ts to enable mode support.</small></div>'}
2251
+ <div class="form-group">
2252
+ <label>Description (optional)</label>
2253
+ <textarea id="token-description">${token.$description || ''}</textarea>
2254
+ </div>
2255
+ ${token.$type === 'color' ? `
2256
+ <div class="token-preview">
2257
+ <div class="token-preview-label">Preview</div>
2258
+ <div class="token-preview-modes">
2259
+ <div class="token-preview-mode">
2260
+ <div class="token-preview-mode-label">Light (Default)</div>
2261
+ <div class="token-preview-mode-swatch" style="background-color: ${isTokenReference(token.$value) ? resolveTokenReference(token.$value) : token.$value}"></div>
2262
+ ${isTokenReference(token.$value) ? `<small style="color: #666; font-size: 9px; cursor: pointer;" onclick="navigateToToken('${token.$value.slice(1, -1)}')">↪ ${token.$value}</small>` : ''}
2263
+ </div>
2264
+ ${(() => {
2265
+ // Collect all modes from both current token and referenced token
2266
+ const allModes = new Set();
2267
+ if (token.$extensions && token.$extensions.$mode) {
2268
+ Object.keys(token.$extensions.$mode).forEach(m => allModes.add(m));
2269
+ }
2270
+ if (isTokenReference(token.$value)) {
2271
+ const refToken = getReferencedToken(token.$value);
2272
+ if (refToken && refToken.$extensions && refToken.$extensions.$mode) {
2273
+ Object.keys(refToken.$extensions.$mode).forEach(m => allModes.add(m));
2274
+ }
2275
+ }
2276
+ return Array.from(allModes).map(modeName => {
2277
+ // Try to get mode value from current token first
2278
+ let modeValue = token.$extensions && token.$extensions.$mode && token.$extensions.$mode[modeName];
2279
+ // If not found and value is a reference, use the reference
2280
+ if (!modeValue && isTokenReference(token.$value)) {
2281
+ modeValue = token.$value;
2282
+ }
2283
+ const resolvedColor = isTokenReference(modeValue) ? resolveTokenReference(modeValue, modeName) : modeValue;
2284
+ return `
2285
+ <div class="token-preview-mode">
2286
+ <div class="token-preview-mode-label">${modeName}</div>
2287
+ <div class="token-preview-mode-swatch" style="background-color: ${resolvedColor}"></div>
2288
+ ${isTokenReference(modeValue) ? `<small style="color: #666; font-size: 9px; cursor: pointer;" onclick="navigateToToken('${modeValue.slice(1, -1)}')">↪ ${modeValue}</small>` : ''}
2289
+ </div>
2290
+ `;
2291
+ }).join('');
2292
+ })()}
2293
+ </div>
2294
+ </div>
2295
+ ` : ''}
2296
+ <div class="form-actions">
2297
+ <button onclick="saveToken()" style="flex: 1; background: var(--figma-color-bg-brand); color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: 500;">Save</button>
2298
+ <button onclick="duplicateToken('${path}')" style="flex: 1; background: var(--figma-color-bg-secondary); color: var(--figma-color-text); border: 1px solid var(--figma-color-border); padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 11px;">Duplicate</button>
2299
+ <button onclick="showEmptyState()" style="background: var(--figma-color-bg-secondary); color: var(--figma-color-text); border: 1px solid var(--figma-color-border); padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 11px;">Cancel</button>
2300
+ <button onclick="if(confirm('Are you sure you want to delete this token?')) deleteToken('${path}')" style="background: #f24822; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 11px;">Remove</button>
2301
+ </div>
2302
+ </div>
2303
+ `;
2304
+
2305
+ editor.innerHTML = form;
2306
+ }
2307
+
2308
+ // Show empty state
2309
+ function showEmptyState() {
2310
+ const editor = document.getElementById('editor-content');
2311
+ editor.innerHTML = `
2312
+ <div class="empty-state">
2313
+ <h3>Welcome to Figma Token Sync</h3>
2314
+ <p>Select a token from the sidebar to edit, or click "Sync to Figma" to synchronize tokens.</p>
2315
+ <p><strong>To import tokens:</strong></p>
2316
+ <ol style="text-align: left; max-width: 450px; margin: 12px auto; line-height: 1.6;">
2317
+ <li>Click <strong>"📂 Import Tokens"</strong></li>
2318
+ <li>Select your <code>tokens</code> folder from your design system</li>
2319
+ <li>All .tokens.json files will be automatically loaded and merged</li>
2320
+ </ol>
2321
+ </div>
2322
+ `;
2323
+ selectedTokenPath = null;
2324
+ renderTokenTree();
2325
+ }
2326
+
2327
+ // Save token changes
2328
+ function saveToken() {
2329
+ if (!selectedTokenPath) return;
2330
+
2331
+ const type = document.getElementById('token-type').value;
2332
+ const value = document.getElementById('token-value').value;
2333
+ const description = document.getElementById('token-description').value;
2334
+
2335
+ // Get mode values if any
2336
+ const modeValues = {};
2337
+ if (config.modes && config.modes.length > 0) {
2338
+ config.modes.forEach(mode => {
2339
+ const modeInput = document.getElementById(`token-value-${mode}`);
2340
+ if (modeInput && modeInput.value) {
2341
+ modeValues[mode] = modeInput.value;
2342
+ }
2343
+ });
2344
+ }
2345
+
2346
+ // Build token object
2347
+ const token = {
2348
+ $type: type,
2349
+ $value: value
2350
+ };
2351
+
2352
+ // Add description if provided
2353
+ if (description) {
2354
+ token.$description = description;
2355
+ }
2356
+
2357
+ // Add mode extensions if any mode values were provided
2358
+ if (Object.keys(modeValues).length > 0) {
2359
+ token.$extensions = {
2360
+ $mode: modeValues
2361
+ };
2362
+ }
2363
+
2364
+ // Update token in the tokens object
2365
+ setNestedValue(tokens, selectedTokenPath, token);
2366
+
2367
+ // Update the token tree and show success
2368
+ renderTokenTree();
2369
+ setStatus('Token saved successfully', 'success');
2370
+
2371
+ // Keep the editor open with updated values
2372
+ showTokenEditor(selectedTokenPath, token);
2373
+ }
2374
+
2375
+ // Delete token
2376
+ function deleteToken(path) {
2377
+ deleteNestedValue(tokens, path);
2378
+ renderTokenTree();
2379
+ showEmptyState();
2380
+ setStatus('Token deleted', 'success');
2381
+ }
2382
+
2383
+ // Duplicate token
2384
+ function duplicateToken(path) {
2385
+ // Get the current token
2386
+ const parts = path.split('.');
2387
+ let token = tokens;
2388
+ for (const part of parts) {
2389
+ if (!token || typeof token !== 'object') return;
2390
+ token = token[part];
2391
+ }
2392
+
2393
+ if (!token || typeof token !== 'object' || !('$type' in token)) return;
2394
+
2395
+ // Get current form values
2396
+ const type = document.getElementById('token-type').value;
2397
+ const value = document.getElementById('token-value').value;
2398
+ const description = document.getElementById('token-description')?.value || '';
2399
+
2400
+ // Get mode values
2401
+ const modeValues = {};
2402
+ if (config.modes && config.modes.length > 0) {
2403
+ config.modes.filter(m => m !== 'default').forEach(mode => {
2404
+ const modeInput = document.getElementById(`token-value-${mode}`);
2405
+ if (modeInput && modeInput.value) {
2406
+ modeValues[mode] = modeInput.value;
2407
+ }
2408
+ });
2409
+ }
2410
+
2411
+ // Create form data for the new token editor
2412
+ const formData = {
2413
+ path: path + '-copy',
2414
+ type: type,
2415
+ value: value,
2416
+ fileIndex: 'new',
2417
+ newFileName: '',
2418
+ modeValues: modeValues
2419
+ };
2420
+
2421
+ // Show new token editor with duplicated data
2422
+ newTokenFormData = formData;
2423
+ showNewTokenEditor(formData);
2424
+
2425
+ setStatus('Token duplicated. Update the path and save.', 'success');
2426
+ }
2427
+
2428
+ // Delete token
2429
+ function deleteToken(path) {
2430
+ deleteNestedValue(tokens, path);
2431
+ renderTokenTree();
2432
+ showEmptyState();
2433
+ setStatus('Token deleted', 'success');
2434
+ }
2435
+
2436
+ // Handle search
2437
+ function handleSearch(e) {
2438
+ searchQuery = e.target.value.toLowerCase();
2439
+ const clearBtn = document.getElementById('search-clear');
2440
+ clearBtn.className = 'search-clear' + (searchQuery ? ' visible' : '');
2441
+ renderTokenTree();
2442
+ }
2443
+
2444
+ // Clear search
2445
+ function clearSearch() {
2446
+ document.getElementById('search-input').value = '';
2447
+ searchQuery = '';
2448
+ document.getElementById('search-clear').className = 'search-clear';
2449
+ renderTokenTree();
2450
+ }
2451
+
2452
+ // Filter tokens
2453
+ function filterTokens(obj, query) {
2454
+ const result = {};
2455
+
2456
+ for (const [key, value] of Object.entries(obj)) {
2457
+ if (key.startsWith('$')) {
2458
+ result[key] = value;
2459
+ continue;
2460
+ }
2461
+
2462
+ if (key.toLowerCase().includes(query)) {
2463
+ result[key] = value;
2464
+ } else if (typeof value === 'object' && !('$type' in value)) {
2465
+ const filtered = filterTokens(value, query);
2466
+ if (Object.keys(filtered).length > 0) {
2467
+ result[key] = filtered;
2468
+ }
2469
+ }
2470
+ }
2471
+
2472
+ return result;
2473
+ }
2474
+
2475
+ // Expand all nodes
2476
+ function expandAll() {
2477
+ expandedNodes.clear();
2478
+ collectAllPaths(tokens, '').forEach(path => expandedNodes.add(path));
2479
+ renderTokenTree();
2480
+ }
2481
+
2482
+ // Collapse all nodes
2483
+ function collapseAll() {
2484
+ expandedNodes.clear();
2485
+ renderTokenTree();
2486
+ }
2487
+
2488
+ // Collect all paths
2489
+ function collectAllPaths(obj, path = '') {
2490
+ const paths = [];
2491
+
2492
+ for (const [key, value] of Object.entries(obj)) {
2493
+ if (key.startsWith('$')) continue;
2494
+
2495
+ const currentPath = path ? `${path}.${key}` : key;
2496
+ const isToken = value && typeof value === 'object' && '$type' in value;
2497
+
2498
+ if (!isToken && typeof value === 'object') {
2499
+ paths.push(currentPath);
2500
+ paths.push(...collectAllPaths(value, currentPath));
2501
+ }
2502
+ }
2503
+
2504
+ return paths;
2505
+ }
2506
+
2507
+ // Set nested value
2508
+ function setNestedValue(obj, path, value) {
2509
+ const parts = path.split('.');
2510
+ let current = obj;
2511
+
2512
+ for (let i = 0; i < parts.length - 1; i++) {
2513
+ if (!current[parts[i]]) {
2514
+ current[parts[i]] = {};
2515
+ }
2516
+ current = current[parts[i]];
2517
+ }
2518
+
2519
+ current[parts[parts.length - 1]] = value;
2520
+ }
2521
+
2522
+ // Delete nested value
2523
+ function deleteNestedValue(obj, path) {
2524
+ const parts = path.split('.');
2525
+ let current = obj;
2526
+
2527
+ for (let i = 0; i < parts.length - 1; i++) {
2528
+ if (!current[parts[i]]) return;
2529
+ current = current[parts[i]];
2530
+ }
2531
+
2532
+ delete current[parts[parts.length - 1]];
2533
+ }
2534
+
2535
+ // View Navigation
2536
+ function showFilesView() {
2537
+ currentView = 'files';
2538
+ document.getElementById('tokens-tab').classList.remove('active');
2539
+ document.getElementById('files-tab').classList.add('active');
2540
+ document.getElementById('config-tab').classList.remove('active');
2541
+ document.getElementById('tokens-view').style.display = 'none';
2542
+ document.getElementById('files-view').classList.add('active');
2543
+ document.getElementById('config-view').classList.remove('active');
2544
+ document.getElementById('editor-content').innerHTML = '';
2545
+ selectedFileIndex = null;
2546
+ renderFilesList();
2547
+ }
2548
+
2549
+ function showTokensView() {
2550
+ currentView = 'tokens';
2551
+ document.getElementById('tokens-tab').classList.add('active');
2552
+ document.getElementById('files-tab').classList.remove('active');
2553
+ document.getElementById('config-tab').classList.remove('active');
2554
+ document.getElementById('tokens-view').style.display = 'flex';
2555
+ document.getElementById('files-view').classList.remove('active');
2556
+ document.getElementById('config-view').classList.remove('active');
2557
+
2558
+ // Restore new token form if it was in progress
2559
+ if (newTokenFormData) {
2560
+ showNewTokenEditor(newTokenFormData);
2561
+ } else {
2562
+ document.getElementById('editor-content').innerHTML = '';
2563
+ selectedTokenPath = null;
2564
+ renderTokenTree();
2565
+ showEmptyState();
2566
+ }
2567
+ }
2568
+
2569
+ function showConfigView() {
2570
+ currentView = 'config';
2571
+ document.getElementById('tokens-tab').classList.remove('active');
2572
+ document.getElementById('files-tab').classList.remove('active');
2573
+ document.getElementById('config-tab').classList.add('active');
2574
+ document.getElementById('tokens-view').style.display = 'none';
2575
+ document.getElementById('files-view').classList.remove('active');
2576
+ document.getElementById('config-view').classList.add('active');
2577
+ document.getElementById('editor-content').innerHTML = '';
2578
+ renderConfigView();
2579
+ }
2580
+
2581
+ function renderFilesList() {
2582
+ const container = document.getElementById('files-list');
2583
+
2584
+ if (!tokenFiles || tokenFiles.length === 0) {
2585
+ container.innerHTML = '<div class="loading">No files imported yet. Click "Import Tokens" to get started.</div>';
2586
+ return;
2587
+ }
2588
+
2589
+ container.innerHTML = '';
2590
+
2591
+ tokenFiles.forEach((fileInfo, index) => {
2592
+ const changes = detectFileChanges(fileInfo);
2593
+ const hasChanges = changes.length > 0;
2594
+
2595
+ const fileItem = document.createElement('div');
2596
+ fileItem.className = 'file-item' + (hasChanges ? ' has-changes' : '') + (selectedFileIndex === index ? ' selected' : '');
2597
+ fileItem.onclick = () => showFileDetail(index);
2598
+
2599
+ const header = document.createElement('div');
2600
+ header.className = 'file-header';
2601
+
2602
+ const name = document.createElement('div');
2603
+ name.className = 'file-name';
2604
+ name.textContent = fileInfo.name;
2605
+ header.appendChild(name);
2606
+
2607
+ if (hasChanges) {
2608
+ const badge = document.createElement('span');
2609
+ badge.className = 'file-badge';
2610
+ badge.textContent = `${changes.length} change${changes.length > 1 ? 's' : ''}`;
2611
+ header.appendChild(badge);
2612
+ }
2613
+
2614
+ fileItem.appendChild(header);
2615
+
2616
+ const path = document.createElement('div');
2617
+ path.className = 'file-path';
2618
+ path.textContent = fileInfo.path;
2619
+ fileItem.appendChild(path);
2620
+
2621
+ container.appendChild(fileItem);
2622
+ });
2623
+ }
2624
+
2625
+ // Alias for backward compatibility
2626
+ const renderFilesView = renderFilesList;
2627
+
2628
+ function renderConfigView() {
2629
+ const container = document.getElementById('config-content');
2630
+
2631
+ if (!configData) {
2632
+ container.innerHTML = `
2633
+ <div class="empty-state" style="padding: 40px;">
2634
+ <h3>No Configuration File</h3>
2635
+ <p>Import a directory with <code>designid.config.ts</code>, <code>designid.config.mjs</code>, or <code>designid.config.js</code> to view configuration settings.</p>
2636
+ </div>
2637
+ `;
2638
+ return;
2639
+ }
2640
+
2641
+ const html = `
2642
+ <div style="padding: 24px;">
2643
+ <div style="margin-bottom: 32px;">
2644
+ <h2 style="margin: 0 0 8px 0; font-size: 18px; font-weight: 600;">⚙️ Configuration</h2>
2645
+ <p style="margin: 0; font-size: 11px; color: var(--figma-color-text-secondary); font-family: 'Monaco', 'Menlo', monospace;">
2646
+ ${configData.fileName}
2647
+ </p>
2648
+ </div>
2649
+
2650
+ <div style="display: grid; gap: 24px;">
2651
+ <div style="background: var(--figma-color-bg-secondary); padding: 16px; border-radius: 8px; border-left: 3px solid #0066ff;">
2652
+ <h4 style="margin: 0 0 12px 0; font-size: 12px; font-weight: 600; color: var(--figma-color-text-secondary); text-transform: uppercase;">Collection Name</h4>
2653
+ <p style="margin: 0; font-size: 14px; font-weight: 500;">${config.collectionName}</p>
2654
+ <p style="margin: 8px 0 0 0; font-size: 10px; color: var(--figma-color-text-secondary);">The name used for the Figma variable collection</p>
2655
+ </div>
2656
+
2657
+ <div style="background: var(--figma-color-bg-secondary); padding: 16px; border-radius: 8px; border-left: 3px solid #14ae5c;">
2658
+ <h4 style="margin: 0 0 12px 0; font-size: 12px; font-weight: 600; color: var(--figma-color-text-secondary); text-transform: uppercase;">Modes</h4>
2659
+ ${config.modes && config.modes.length > 0 ? `
2660
+ <div style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px;">
2661
+ ${config.modes.map(mode => `
2662
+ <span style="padding: 4px 12px; background: var(--figma-color-bg-brand); color: white; border-radius: 12px; font-size: 11px; font-weight: 500; display: flex; align-items: center; gap: 6px;">
2663
+ ${mode}
2664
+ ${mode !== 'default' ? `<button onclick="removeMode('${mode}')" style="background: none; border: none; color: white; cursor: pointer; padding: 0; margin: 0; font-size: 14px; line-height: 1;">×</button>` : ''}
2665
+ </span>
2666
+ `).join('')}
2667
+ </div>
2668
+ <p style="margin: 8px 0 0 0; font-size: 10px; color: var(--figma-color-text-secondary);">${config.modes.length} mode${config.modes.length > 1 ? 's' : ''} configured</p>
2669
+ ` : `
2670
+ <div style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px;">
2671
+ <span style="padding: 4px 12px; background: var(--figma-color-bg-brand); color: white; border-radius: 12px; font-size: 11px; font-weight: 500;">default</span>
2672
+ </div>
2673
+ <p style="margin: 8px 0 0 0; font-size: 10px; color: var(--figma-color-text-secondary);">1 mode configured (default only)</p>
2674
+ `}
2675
+ <div style="margin-top: 12px; display: flex; gap: 8px;">
2676
+ <input type="text" id="new-mode-name" placeholder="Mode name (e.g., dark)" style="flex: 1; padding: 6px 8px; border: 1px solid var(--figma-color-border); border-radius: 4px; font-size: 11px;" />
2677
+ <button onclick="addMode()" style="padding: 6px 12px; background: var(--figma-color-bg-brand); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: 500;">Add Mode</button>
2678
+ </div>
2679
+ </div>
2680
+
2681
+ <div style="background: var(--figma-color-bg-secondary); padding: 16px; border-radius: 8px; border-left: 3px solid #ffcd29;">
2682
+ <h4 style="margin: 0 0 12px 0; font-size: 12px; font-weight: 600; color: var(--figma-color-text-secondary); text-transform: uppercase;">Base Pixel Size</h4>
2683
+ <p style="margin: 0; font-size: 14px; font-weight: 500;">${config.basePixelSize}px</p>
2684
+ <p style="margin: 8px 0 0 0; font-size: 10px; color: var(--figma-color-text-secondary);">Used for rem/px conversion in dimension tokens</p>
2685
+ </div>
2686
+
2687
+ <div style="background: var(--figma-color-bg-secondary); padding: 16px; border-radius: 8px; border-left: 3px solid #8b5cf6;">
2688
+ <h4 style="margin: 0 0 12px 0; font-size: 12px; font-weight: 600; color: var(--figma-color-text-secondary); text-transform: uppercase;">Token Files</h4>
2689
+ <p style="margin: 0 0 12px 0; font-size: 14px; font-weight: 500;">${tokenFiles.length} file${tokenFiles.length !== 1 ? 's' : ''} imported</p>
2690
+ ${tokenFiles.length > 0 ? `
2691
+ <div style="display: flex; flex-direction: column; gap: 4px;">
2692
+ ${tokenFiles.map(file => `
2693
+ <div style="font-size: 11px; font-family: 'Monaco', 'Menlo', monospace; color: var(--figma-color-text-secondary);">
2694
+ 📄 ${file.name}
2695
+ </div>
2696
+ `).join('')}
2697
+ </div>
2698
+ ` : ''}
2699
+ </div>
2700
+
2701
+ <div style="background: var(--figma-color-bg-secondary); padding: 16px; border-radius: 8px; border-left: 3px solid #f24822;">
2702
+ <h4 style="margin: 0 0 12px 0; font-size: 12px; font-weight: 600; color: var(--figma-color-text-secondary); text-transform: uppercase;">Raw Configuration</h4>
2703
+ <details style="cursor: pointer;">
2704
+ <summary style="font-size: 11px; margin-bottom: 12px; color: var(--figma-color-text);">View source code</summary>
2705
+ <pre style="margin: 12px 0 0 0; padding: 12px; background: var(--figma-color-bg); border-radius: 4px; font-size: 10px; font-family: 'Monaco', 'Menlo', monospace; overflow-x: auto; max-height: 400px; overflow-y: auto;">${configData.content.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</pre>
2706
+ </details>
2707
+ </div>
2708
+
2709
+ </div>
2710
+
2711
+ <div style="margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--figma-color-border);">
2712
+ <button onclick="exportConfigFile()" style="width: 100%; padding: 12px; background: var(--figma-color-bg-brand); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500;">💾 Export Config File</button>
2713
+ </div>
2714
+ </div>
2715
+ `;
2716
+
2717
+ container.innerHTML = html;
2718
+ } function detectFileChanges(fileInfo) {
2719
+ const changes = [];
2720
+ const original = fileInfo.tokens;
2721
+ const current = tokens;
2722
+
2723
+ // Deep compare tokens
2724
+ function compareTokens(originalObj, currentObj, path = []) {
2725
+ for (const key in originalObj) {
2726
+ if (key.startsWith('$')) continue;
2727
+
2728
+ const fullPath = [...path, key];
2729
+ const originalValue = originalObj[key];
2730
+
2731
+ // Find current value in merged tokens
2732
+ let currentValue = current;
2733
+ for (const part of fullPath) {
2734
+ if (!currentValue || typeof currentValue !== 'object') {
2735
+ currentValue = undefined;
2736
+ break;
2737
+ }
2738
+ currentValue = currentValue[part];
2739
+ }
2740
+
2741
+ if (originalValue && typeof originalValue === 'object' && '$type' in originalValue) {
2742
+ // This is a token - compare values (case-insensitive for strings)
2743
+ if (!currentValue) {
2744
+ changes.push({
2745
+ type: 'removed',
2746
+ path: fullPath.join('.'),
2747
+ oldValue: originalValue.$value,
2748
+ newValue: null
2749
+ });
2750
+ } else {
2751
+ // Normalize values for comparison (case-insensitive)
2752
+ const normalizeValue = (val) => {
2753
+ if (typeof val === 'string') {
2754
+ return val.toLowerCase();
2755
+ }
2756
+ if (val && typeof val === 'object') {
2757
+ const normalized = {};
2758
+ for (const k in val) {
2759
+ normalized[k] = normalizeValue(val[k]);
2760
+ }
2761
+ return normalized;
2762
+ }
2763
+ return val;
2764
+ };
2765
+
2766
+ const originalNormalized = JSON.stringify(normalizeValue(originalValue));
2767
+ const currentNormalized = JSON.stringify(normalizeValue(currentValue));
2768
+
2769
+ if (originalNormalized !== currentNormalized) {
2770
+ changes.push({
2771
+ type: 'changed',
2772
+ path: fullPath.join('.'),
2773
+ oldValue: originalValue.$value,
2774
+ newValue: currentValue.$value,
2775
+ oldToken: originalValue,
2776
+ newToken: currentValue
2777
+ });
2778
+ }
2779
+ }
2780
+ } else if (originalValue && typeof originalValue === 'object') {
2781
+ // This is a group - recurse
2782
+ compareTokens(originalValue, currentObj, fullPath);
2783
+ }
2784
+ }
2785
+
2786
+ // Check for added tokens
2787
+ if (currentObj && typeof currentObj === 'object') {
2788
+ for (const key in currentObj) {
2789
+ if (key.startsWith('$')) continue;
2790
+
2791
+ const fullPath = [...path, key];
2792
+ const currentValue = currentObj[key];
2793
+
2794
+ if (currentValue && typeof currentValue === 'object' && '$type' in currentValue) {
2795
+ // Check if this token exists in original
2796
+ let originalValue = original;
2797
+ for (const part of fullPath) {
2798
+ if (!originalValue || typeof originalValue !== 'object') {
2799
+ originalValue = undefined;
2800
+ break;
2801
+ }
2802
+ originalValue = originalValue[part];
2803
+ }
2804
+
2805
+ if (!originalValue) {
2806
+ changes.push({
2807
+ type: 'added',
2808
+ path: fullPath.join('.'),
2809
+ oldValue: null,
2810
+ newValue: currentValue.$value,
2811
+ newToken: currentValue
2812
+ });
2813
+ }
2814
+ }
2815
+ }
2816
+ }
2817
+ }
2818
+
2819
+ compareTokens(original, current);
2820
+ return changes;
2821
+ }
2822
+
2823
+ // Add new mode
2824
+ function addMode() {
2825
+ const input = document.getElementById('new-mode-name');
2826
+ const modeName = input.value.trim();
2827
+
2828
+ if (!modeName) {
2829
+ alert('Please enter a mode name');
2830
+ return;
2831
+ }
2832
+
2833
+ if (modeName === 'default') {
2834
+ alert('"default" is a reserved mode name');
2835
+ return;
2836
+ }
2837
+
2838
+ if (config.modes.includes(modeName)) {
2839
+ alert(`Mode "${modeName}" already exists`);
2840
+ return;
2841
+ }
2842
+
2843
+ config.modes.push(modeName);
2844
+ input.value = '';
2845
+ updateModesIndicator();
2846
+ renderConfigView();
2847
+ setStatus(`Mode "${modeName}" added`, 'success');
2848
+ }
2849
+
2850
+ // Remove mode
2851
+ function removeMode(modeName) {
2852
+ if (modeName === 'default') {
2853
+ alert('Cannot remove the default mode');
2854
+ return;
2855
+ }
2856
+
2857
+ if (!confirm(`Remove mode "${modeName}"? This will not affect existing token values.`)) {
2858
+ return;
2859
+ }
2860
+
2861
+ config.modes = config.modes.filter(m => m !== modeName);
2862
+ updateModesIndicator();
2863
+ renderConfigView();
2864
+ setStatus(`Mode "${modeName}" removed`, 'success');
2865
+ }
2866
+
2867
+ // Export config file
2868
+ function exportConfigFile() {
2869
+ if (!configData) {
2870
+ // Create a new config from current settings
2871
+ const newConfig = {
2872
+ $name: config.collectionName,
2873
+ $modes: {},
2874
+ basePixelSize: config.basePixelSize
2875
+ };
2876
+
2877
+ // Add all modes
2878
+ const allModes = config.modes.includes('default') ? config.modes : ['default', ...config.modes];
2879
+ allModes.forEach(mode => {
2880
+ newConfig.$modes[mode] = {};
2881
+ });
2882
+
2883
+ const configContent = `export default ${JSON.stringify(newConfig, null, 2)};`;
2884
+
2885
+ const blob = new Blob([configContent], { type: 'text/javascript' });
2886
+ const url = URL.createObjectURL(blob);
2887
+ const a = document.createElement('a');
2888
+ a.href = url;
2889
+ a.download = 'designid.config.mjs';
2890
+ a.click();
2891
+ URL.revokeObjectURL(url);
2892
+
2893
+ setStatus('Config file exported', 'success');
2894
+ return;
2895
+ }
2896
+
2897
+ // Update existing config with current modes
2898
+ let updatedConfig = configData.content;
2899
+
2900
+ // Update $name
2901
+ updatedConfig = updatedConfig.replace(
2902
+ /\$name\s*:\s*['"][^'"]+['"]/,
2903
+ `\$name: '${config.collectionName}'`
2904
+ );
2905
+
2906
+ // Update $modes section
2907
+ const allModes = config.modes.includes('default') ? config.modes : ['default', ...config.modes];
2908
+ const modesObj = {};
2909
+ allModes.forEach(mode => {
2910
+ modesObj[mode] = {};
2911
+ });
2912
+
2913
+ const modesStr = JSON.stringify(modesObj, null, 4).replace(/^/gm, ' ');
2914
+ updatedConfig = updatedConfig.replace(
2915
+ /\$modes\s*:\s*\{[^}]+\}/s,
2916
+ `\$modes: ${modesStr.trim()}`
2917
+ );
2918
+
2919
+ const blob = new Blob([updatedConfig], { type: 'text/javascript' });
2920
+ const url = URL.createObjectURL(blob);
2921
+ const a = document.createElement('a');
2922
+ a.href = url;
2923
+ a.download = configData.fileName;
2924
+ a.click();
2925
+ URL.revokeObjectURL(url);
2926
+
2927
+ setStatus('Config file exported', 'success');
2928
+ }
2929
+
2930
+ function showFileDetail(index) {
2931
+ selectedFileIndex = index;
2932
+ renderFilesList();
2933
+
2934
+ const fileInfo = tokenFiles[index];
2935
+ const changes = detectFileChanges(fileInfo);
2936
+ const editor = document.getElementById('editor-content');
2937
+
2938
+ const html = `
2939
+ <div class="file-detail active">
2940
+ <div class="file-detail-header">
2941
+ <div class="file-detail-title">
2942
+ <h3>${fileInfo.name}</h3>
2943
+ <p>${fileInfo.path}</p>
2944
+ </div>
2945
+ <div class="file-detail-actions">
2946
+ <button onclick="exportFile(${index})">💾 Export File</button>
2947
+ </div>
2948
+ </div>
2949
+ <div class="file-detail-content">
2950
+ ${changes.length === 0 ? `
2951
+ <div class="empty-state">
2952
+ <h3>No Changes</h3>
2953
+ <p>This file has no changes from the original import.</p>
2954
+ </div>
2955
+ ` : `
2956
+ <div class="diff-section">
2957
+ <h4>Changes (${changes.length})</h4>
2958
+ ${changes.map(change => `
2959
+ <div class="diff-item ${change.type}">
2960
+ <div class="diff-path">${change.path}</div>
2961
+ <div class="diff-values">
2962
+ ${change.oldValue !== null ? `
2963
+ <div class="diff-old">
2964
+ <div class="diff-label">Original</div>
2965
+ <div>${formatTokenValue(change.oldValue)}</div>
2966
+ ${change.oldToken && change.oldToken.$extensions && change.oldToken.$extensions.$mode ? Object.entries(change.oldToken.$extensions.$mode).map(([mode, value]) => `
2967
+ <div style="margin-top: 4px; opacity: 0.7;">${mode}: ${formatTokenValue(value)}</div>
2968
+ `).join('') : ''}
2969
+ </div>
2970
+ ` : '<div class="diff-old"><div class="diff-label">Original</div><div style="opacity: 0.5;">—</div></div>'}
2971
+ ${change.newValue !== null ? `
2972
+ <div class="diff-new">
2973
+ <div class="diff-label">Current</div>
2974
+ <div>${formatTokenValue(change.newValue)}</div>
2975
+ ${change.newToken && change.newToken.$extensions && change.newToken.$extensions.$mode ? Object.entries(change.newToken.$extensions.$mode).map(([mode, value]) => `
2976
+ <div style="margin-top: 4px; opacity: 0.7;">${mode}: ${formatTokenValue(value)}</div>
2977
+ `).join('') : ''}
2978
+ </div>
2979
+ ` : '<div class="diff-new"><div class="diff-label">Current</div><div style="opacity: 0.5;">—</div></div>'}
2980
+ </div>
2981
+ </div>
2982
+ `).join('')}
2983
+ </div>
2984
+ `}
2985
+ </div>
2986
+ </div>
2987
+ `;
2988
+
2989
+ editor.innerHTML = html;
2990
+ }
2991
+
2992
+ function exportFile(index) {
2993
+ const fileInfo = tokenFiles[index];
2994
+ const updatedTokens = updateTokenValues(fileInfo.tokens, tokens);
2995
+ const content = JSON.stringify(updatedTokens, null, 2);
2996
+
2997
+ const blob = new Blob([content], { type: 'application/json' });
2998
+ const url = URL.createObjectURL(blob);
2999
+ const a = document.createElement('a');
3000
+ a.href = url;
3001
+ a.download = fileInfo.name;
3002
+ a.click();
3003
+ URL.revokeObjectURL(url);
3004
+
3005
+ setStatus(`Exported ${fileInfo.name}`, 'success');
3006
+ }
3007
+ </script>
3008
+ </body>
3009
+ </html>