skimmd 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # skimmd
1
+ ### skimmd
2
2
 
3
3
  **One command. Zero install. Your markdown, beautifully rendered.**
4
4
 
@@ -8,20 +8,20 @@ npx skimmd
8
8
 
9
9
  That's it. Browser opens. Your markdown files look like GitHub. Edit them live.
10
10
 
11
- ---
11
+ * * *
12
12
 
13
13
  ## The Problem
14
14
 
15
15
  You're writing a README. You want to see how it looks on GitHub. Your options:
16
16
 
17
- 1. Push to GitHub, refresh, fix typo, push again, refresh... (we've all been there)
18
- 2. Install a 200MB Electron app
19
- 3. Use VS Code's preview that looks nothing like GitHub
20
- 4. Use `grip` but hit GitHub API rate limits
17
+ 1. Push to GitHub, refresh, fix typo, push again, refresh... (we've all been there)
18
+ 2. Install a 200MB Electron app
19
+ 3. Use VS Code's preview that looks nothing like GitHub
20
+ 4. Use `grip` but hit GitHub API rate limits
21
21
 
22
22
  **skimmd**: One command, works offline, looks like GitHub, updates as you type.
23
23
 
24
- ---
24
+ * * *
25
25
 
26
26
  ## 30-Second Demo
27
27
 
@@ -34,64 +34,102 @@ Your browser opens. You see your README rendered beautifully. Now open that file
34
34
 
35
35
  No reload button. No refresh. Just save and see.
36
36
 
37
- ---
37
+ ### Dark Mode
38
+
39
+ ![Dark Mode](darkMode.png)
40
+
41
+ ### Light Mode
42
+
43
+ ![Light Mode](lightMode.png)
44
+
45
+ * * *
38
46
 
39
47
  ## Features
40
48
 
41
49
  ### Live Reload
50
+
42
51
  Edit in VS Code, Vim, or any editor. Save. Browser updates. No plugins, no extensions, no configuration.
43
52
 
44
53
  ### Inline Editing
54
+
45
55
  Click "Edit" in the browser. Make changes. Hit Ctrl+S. **Saves directly to your .md file.** Your editor picks up the change. Round-trip editing.
46
56
 
47
57
  ### GitHub-Flavored Markdown
48
- - Tables render correctly
49
- - Task lists work (`- [x] like this`)
50
- - Syntax highlighting for 190+ languages
51
- - Fenced code blocks
52
- - Strikethrough, autolinks, all of it
58
+
59
+ - Tables render correctly
60
+ - Task lists work (`- [x] like this`)
61
+ - Syntax highlighting for 190+ languages
62
+ - Fenced code blocks
63
+ - Strikethrough, autolinks, all of it
53
64
 
54
65
  ### Dark Mode
66
+
55
67
  Light, Dark, or Auto (follows your system). Because it's 2024 and we're not animals.
56
68
 
57
69
  ### Browse Entire Folders
70
+
58
71
  ```bash
59
72
  npx skimmd ./docs
60
73
  ```
74
+
61
75
  Sidebar shows all your markdown files. Filter them. Jump between them. Table of contents auto-generated from headings.
62
76
 
63
- ---
77
+ * * *
64
78
 
65
79
  ## Why Not Just Use...
66
80
 
67
- | Tool | The Problem |
68
- |------|-------------|
69
- | **VS Code Preview** | Doesn't look like GitHub. No live reload in browser. |
70
- | **grip** | Hits GitHub API. Rate limited. Requires auth for private repos. |
71
- | **Typora** | $15. Electron app. Doesn't look like GitHub. |
72
- | **MacDown** | macOS only. No live reload. Dated UI. |
73
- | **Obsidian** | Overkill for previewing a README. Different styling. |
74
- | **glow** | Terminal only. Can't share screen with non-terminal people. |
75
- | **peekmd** | Requires Bun. No editing. No live reload. |
81
+ Tool
82
+
83
+ The Problem
84
+
85
+ **VS Code Preview**
86
+
87
+ Doesn't look like GitHub. No live reload in browser.
88
+
89
+ **grip**
90
+
91
+ Hits GitHub API. Rate limited. Requires auth for private repos.
92
+
93
+ **Typora**
94
+
95
+ $15. Electron app. Doesn't look like GitHub.
96
+
97
+ **MacDown**
98
+
99
+ macOS only. No live reload. Dated UI.
100
+
101
+ **Obsidian**
102
+
103
+ Overkill for previewing a README. Different styling.
104
+
105
+ **glow**
106
+
107
+ Terminal only. Can't share screen with non-terminal people.
108
+
109
+ **peekmd**
110
+
111
+ Requires Bun. No editing. No live reload.
76
112
 
77
113
  **skimmd**: Works with Node (you already have it). Zero config. Live reload. Inline editing. Looks like GitHub.
78
114
 
79
- ---
115
+ * * *
80
116
 
81
117
  ## Installation
82
118
 
83
119
  **Option 1: No install (recommended)**
120
+
84
121
  ```bash
85
122
  npx skimmd
86
123
  ```
87
124
 
88
125
  **Option 2: Global install**
126
+
89
127
  ```bash
90
128
  npm install -g skimmd
91
129
  skimmd
92
130
  ```
93
131
 
94
- ---
132
+ * * *
95
133
 
96
134
  ## Usage
97
135
 
@@ -112,48 +150,50 @@ skimmd --port 8080
112
150
  skimmd --no-open
113
151
  ```
114
152
 
115
- ---
153
+ * * *
116
154
 
117
155
  ## Keyboard Shortcuts
118
156
 
119
- | Shortcut | Action |
120
- |----------|--------|
121
- | `Ctrl/Cmd + S` | Save (when editing) |
122
- | `Ctrl/Cmd + K` | Focus file filter |
157
+ Shortcut
158
+
159
+ Action
160
+
161
+ `Ctrl/Cmd + S`
162
+
163
+ Save (when editing)
164
+
165
+ `Ctrl/Cmd + K`
166
+
167
+ Focus file filter
123
168
 
124
- ---
169
+ * * *
125
170
 
126
171
  ## How It Works
127
172
 
128
- 1. Starts a local Express server
129
- 2. Watches your markdown files with chokidar
130
- 3. Renders with `marked` (GitHub-flavored)
131
- 4. Syntax highlighting with `highlight.js`
132
- 5. Sends file changes via Server-Sent Events
133
- 6. Browser updates without refresh
173
+ 1. Starts a local Express server
174
+ 2. Watches your markdown files with chokidar
175
+ 3. Renders with `marked` (GitHub-flavored)
176
+ 4. Syntax highlighting with `highlight.js`
177
+ 5. Sends file changes via Server-Sent Events
178
+ 6. Browser updates without refresh
134
179
 
135
180
  All local. Nothing leaves your machine. Works on a plane.
136
181
 
137
- ---
182
+ * * *
138
183
 
139
184
  ## FAQ
140
185
 
141
- **Does it work offline?**
142
- Yes. Everything runs locally. No API calls.
186
+ **Does it work offline?** Yes. Everything runs locally. No API calls.
143
187
 
144
- **Can I use it for a presentation?**
145
- Yes. Dark mode + clean UI + live reload = great for live coding demos.
188
+ **Can I use it for a presentation?** Yes. Dark mode + clean UI + live reload = great for live coding demos.
146
189
 
147
- **Does it support Mermaid diagrams?**
148
- Not yet. PRs welcome.
190
+ **Does it support Mermaid diagrams?** Not yet. PRs welcome.
149
191
 
150
- **What about MDX?**
151
- Just markdown for now. Keep it simple.
192
+ **What about MDX?** Just markdown for now. Keep it simple.
152
193
 
153
- **Windows support?**
154
- Yes. Works anywhere Node.js runs.
194
+ **Windows support?** Yes. Works anywhere Node.js runs.
155
195
 
156
- ---
196
+ * * *
157
197
 
158
198
  ## Contributing
159
199
 
@@ -167,15 +207,13 @@ npm link
167
207
  skimmd ./docs
168
208
  ```
169
209
 
170
- ---
210
+ * * *
171
211
 
172
212
  ## License
173
213
 
174
214
  MIT
175
215
 
176
- ---
216
+ * * *
177
217
 
178
- <p align="center">
179
- <b>Stop pushing to GitHub just to preview your README.</b><br>
180
- <code>npx skimmd</code>
181
- </p>
218
+ **Stop pushing to GitHub just to preview your README.**
219
+ `npx skimmd`
package/bin/skimmd.js CHANGED
@@ -363,9 +363,23 @@ program
363
363
  }
364
364
  });
365
365
 
366
- const shutdown = () => {
367
- watcher.close();
366
+ const shutdown = async () => {
367
+ console.log("\nShutting down...");
368
+
369
+ // Close all SSE connections first
370
+ for (const client of sseClients) {
371
+ client.end();
372
+ }
373
+ sseClients.clear();
374
+
375
+ // Close file watcher (returns Promise in chokidar v5)
376
+ await watcher.close();
377
+
378
+ // Close server with timeout fallback
368
379
  server.close(() => process.exit(0));
380
+
381
+ // Force exit if server doesn't close within 2 seconds
382
+ setTimeout(() => process.exit(0), 2000);
369
383
  };
370
384
 
371
385
  process.on("SIGINT", shutdown);
package/darkMode.png ADDED
Binary file
package/lightMode.png ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skimmd",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Instant markdown preview in your browser. Zero config, GitHub-style rendering, inline editing.",
5
5
  "type": "module",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -247,8 +247,8 @@
247
247
  padding: 20px;
248
248
  box-shadow: var(--shadow);
249
249
  overflow: hidden;
250
- display: grid;
251
- grid-template-rows: auto 1fr;
250
+ display: flex;
251
+ flex-direction: column;
252
252
  gap: 16px;
253
253
  min-height: 0;
254
254
  max-height: 100vh;
@@ -334,13 +334,7 @@
334
334
  gap: 16px;
335
335
  min-height: 0;
336
336
  overflow-y: auto;
337
- }
338
-
339
- .document {
340
- display: flex;
341
- flex-direction: column;
342
- gap: 16px;
343
- min-height: 0;
337
+ flex: 1;
344
338
  }
345
339
 
346
340
  .doc-header {
@@ -497,10 +491,8 @@
497
491
  list-style: none;
498
492
  padding: 0;
499
493
  margin: 0;
500
- overflow-y: auto;
501
494
  display: grid;
502
495
  gap: 6px;
503
- max-height: 300px;
504
496
  }
505
497
 
506
498
  .toc-item {
@@ -558,6 +550,300 @@
558
550
  display: none;
559
551
  }
560
552
 
553
+ /* Formatting Toolbar */
554
+ .format-toolbar {
555
+ display: none;
556
+ align-items: center;
557
+ gap: 4px;
558
+ padding: 8px 12px;
559
+ background: var(--panel);
560
+ border: 1px solid var(--border);
561
+ border-radius: var(--radius-sm);
562
+ margin-bottom: 12px;
563
+ flex-wrap: wrap;
564
+ }
565
+
566
+ body.editing .format-toolbar {
567
+ display: flex;
568
+ }
569
+
570
+ .format-toolbar .divider {
571
+ width: 1px;
572
+ height: 20px;
573
+ background: var(--border);
574
+ margin: 0 6px;
575
+ }
576
+
577
+ .format-btn {
578
+ display: flex;
579
+ align-items: center;
580
+ justify-content: center;
581
+ width: 32px;
582
+ height: 32px;
583
+ border: none;
584
+ background: transparent;
585
+ border-radius: 6px;
586
+ cursor: pointer;
587
+ color: var(--ink);
588
+ font-family: var(--font-ui);
589
+ font-size: 14px;
590
+ transition: background 0.15s ease, color 0.15s ease;
591
+ }
592
+
593
+ .format-btn:hover {
594
+ background: var(--hover-bg);
595
+ }
596
+
597
+ .format-btn.active {
598
+ background: var(--accent-soft);
599
+ color: var(--accent);
600
+ }
601
+
602
+ .format-btn svg {
603
+ width: 18px;
604
+ height: 18px;
605
+ stroke: currentColor;
606
+ stroke-width: 2;
607
+ fill: none;
608
+ }
609
+
610
+ .format-btn.bold {
611
+ font-weight: 700;
612
+ }
613
+
614
+ .format-btn.italic {
615
+ font-style: italic;
616
+ }
617
+
618
+ .format-btn.strike {
619
+ text-decoration: line-through;
620
+ }
621
+
622
+ .format-dropdown {
623
+ position: relative;
624
+ }
625
+
626
+ .format-dropdown-btn {
627
+ display: flex;
628
+ align-items: center;
629
+ gap: 4px;
630
+ padding: 6px 10px;
631
+ border: none;
632
+ background: transparent;
633
+ border-radius: 6px;
634
+ cursor: pointer;
635
+ color: var(--ink);
636
+ font-family: var(--font-ui);
637
+ font-size: 13px;
638
+ transition: background 0.15s ease;
639
+ }
640
+
641
+ .format-dropdown-btn:hover {
642
+ background: var(--hover-bg);
643
+ }
644
+
645
+ .format-dropdown-btn svg {
646
+ width: 12px;
647
+ height: 12px;
648
+ stroke: currentColor;
649
+ stroke-width: 2;
650
+ fill: none;
651
+ }
652
+
653
+ .format-dropdown-menu {
654
+ display: none;
655
+ position: absolute;
656
+ top: 100%;
657
+ left: 0;
658
+ margin-top: 4px;
659
+ min-width: 140px;
660
+ background: var(--panel);
661
+ border: 1px solid var(--border);
662
+ border-radius: var(--radius-sm);
663
+ box-shadow: var(--shadow);
664
+ z-index: 100;
665
+ padding: 4px;
666
+ }
667
+
668
+ .format-dropdown.open .format-dropdown-menu {
669
+ display: block;
670
+ }
671
+
672
+ .format-dropdown-item {
673
+ display: block;
674
+ width: 100%;
675
+ padding: 8px 12px;
676
+ border: none;
677
+ background: transparent;
678
+ border-radius: 4px;
679
+ cursor: pointer;
680
+ color: var(--ink);
681
+ font-family: var(--font-ui);
682
+ font-size: 13px;
683
+ text-align: left;
684
+ transition: background 0.15s ease;
685
+ }
686
+
687
+ .format-dropdown-item:hover {
688
+ background: var(--hover-bg);
689
+ }
690
+
691
+ .format-dropdown-item.h1 {
692
+ font-size: 18px;
693
+ font-weight: 700;
694
+ }
695
+
696
+ .format-dropdown-item.h2 {
697
+ font-size: 16px;
698
+ font-weight: 600;
699
+ }
700
+
701
+ .format-dropdown-item.h3 {
702
+ font-size: 14px;
703
+ font-weight: 600;
704
+ }
705
+
706
+ .word-count {
707
+ margin-left: auto;
708
+ font-size: 12px;
709
+ color: var(--muted);
710
+ padding: 0 8px;
711
+ }
712
+
713
+ /* Width toggle buttons */
714
+ .width-toggle {
715
+ display: flex;
716
+ gap: 2px;
717
+ padding: 2px;
718
+ background: var(--pill-bg);
719
+ border-radius: 6px;
720
+ border: 1px solid var(--border);
721
+ }
722
+
723
+ .width-btn {
724
+ padding: 4px 8px;
725
+ border: none;
726
+ background: transparent;
727
+ border-radius: 4px;
728
+ cursor: pointer;
729
+ font-size: 11px;
730
+ color: var(--muted);
731
+ transition: background 0.15s ease, color 0.15s ease;
732
+ font-family: var(--font-ui);
733
+ }
734
+
735
+ .width-btn:hover {
736
+ color: var(--ink);
737
+ }
738
+
739
+ .width-btn.active {
740
+ background: var(--panel);
741
+ color: var(--ink);
742
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
743
+ }
744
+
745
+ /* Focus button */
746
+ .focus-btn {
747
+ display: flex;
748
+ align-items: center;
749
+ gap: 4px;
750
+ padding: 6px 10px;
751
+ border: none;
752
+ background: transparent;
753
+ border-radius: 6px;
754
+ cursor: pointer;
755
+ color: var(--muted);
756
+ font-family: var(--font-ui);
757
+ font-size: 12px;
758
+ transition: background 0.15s ease, color 0.15s ease;
759
+ }
760
+
761
+ .focus-btn:hover {
762
+ background: var(--hover-bg);
763
+ color: var(--ink);
764
+ }
765
+
766
+ .focus-btn svg {
767
+ width: 14px;
768
+ height: 14px;
769
+ stroke: currentColor;
770
+ stroke-width: 2;
771
+ fill: none;
772
+ }
773
+
774
+ .focus-btn .exit-icon,
775
+ .focus-btn .exit-text {
776
+ display: none;
777
+ }
778
+
779
+ body.focus-mode .focus-btn .focus-icon,
780
+ body.focus-mode .focus-btn .focus-text {
781
+ display: none;
782
+ }
783
+
784
+ body.focus-mode .focus-btn .exit-icon,
785
+ body.focus-mode .focus-btn .exit-text {
786
+ display: inline;
787
+ }
788
+
789
+ /* Document section */
790
+ .document {
791
+ display: flex;
792
+ flex-direction: column;
793
+ gap: 16px;
794
+ min-height: 0;
795
+ }
796
+
797
+ /* Format toolbar - positioned outside scroll area so it stays fixed */
798
+ body.editing .format-toolbar {
799
+ display: flex;
800
+ z-index: 50;
801
+ box-shadow: var(--shadow-soft);
802
+ flex-shrink: 0;
803
+ }
804
+
805
+ /* Focus mode */
806
+ body.focus-mode .app {
807
+ grid-template-columns: 1fr;
808
+ }
809
+
810
+ body.focus-mode .sidebar {
811
+ display: none;
812
+ }
813
+
814
+ body.focus-mode .viewer-body {
815
+ grid-template-columns: minmax(0, 1fr) 260px;
816
+ }
817
+
818
+ body.focus-mode .viewer {
819
+ max-width: 100%;
820
+ border: none;
821
+ box-shadow: none;
822
+ border-radius: 0;
823
+ }
824
+
825
+ body.focus-mode .viewer-header {
826
+ display: none;
827
+ }
828
+
829
+ body.focus-mode .doc-header {
830
+ display: none;
831
+ }
832
+
833
+ body.focus-mode .inspector {
834
+ display: none;
835
+ }
836
+
837
+ /* Content width modes - only apply in editing mode */
838
+ body.editing .content.fixed-width {
839
+ max-width: 720px;
840
+ margin: 0 auto;
841
+ }
842
+
843
+ body.editing .content.full-width {
844
+ max-width: none;
845
+ }
846
+
561
847
  .theme-toggle {
562
848
  display: flex;
563
849
  gap: 4px;
@@ -661,6 +947,62 @@
661
947
  <button class="button secondary hidden" id="cancel">Cancel</button>
662
948
  </div>
663
949
  </div>
950
+ <div class="format-toolbar" id="format-toolbar">
951
+ <button class="format-btn bold" data-cmd="bold" title="Bold (Cmd+B)">B</button>
952
+ <button class="format-btn italic" data-cmd="italic" title="Italic (Cmd+I)">I</button>
953
+ <button class="format-btn strike" data-cmd="strikeThrough" title="Strikethrough">S</button>
954
+ <div class="divider"></div>
955
+ <div class="format-dropdown" id="heading-dropdown">
956
+ <button class="format-dropdown-btn" id="heading-btn">
957
+ <span id="heading-label">Body</span>
958
+ <svg viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"></polyline></svg>
959
+ </button>
960
+ <div class="format-dropdown-menu">
961
+ <button class="format-dropdown-item h1" data-heading="h1">Heading 1</button>
962
+ <button class="format-dropdown-item h2" data-heading="h2">Heading 2</button>
963
+ <button class="format-dropdown-item h3" data-heading="h3">Heading 3</button>
964
+ <button class="format-dropdown-item" data-heading="p">Body</button>
965
+ </div>
966
+ </div>
967
+ <div class="divider"></div>
968
+ <button class="format-btn" data-cmd="formatBlock" data-value="blockquote" title="Blockquote">
969
+ <svg viewBox="0 0 24 24"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V21z"></path><path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"></path></svg>
970
+ </button>
971
+ <button class="format-btn" data-cmd="insertUnorderedList" title="Bullet List">
972
+ <svg viewBox="0 0 24 24"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><circle cx="4" cy="6" r="1" fill="currentColor"></circle><circle cx="4" cy="12" r="1" fill="currentColor"></circle><circle cx="4" cy="18" r="1" fill="currentColor"></circle></svg>
973
+ </button>
974
+ <button class="format-btn" data-cmd="insertOrderedList" title="Numbered List">
975
+ <svg viewBox="0 0 24 24"><line x1="10" y1="6" x2="21" y2="6"></line><line x1="10" y1="12" x2="21" y2="12"></line><line x1="10" y1="18" x2="21" y2="18"></line><text x="4" y="7" font-size="6" fill="currentColor" stroke="none">1</text><text x="4" y="13" font-size="6" fill="currentColor" stroke="none">2</text><text x="4" y="19" font-size="6" fill="currentColor" stroke="none">3</text></svg>
976
+ </button>
977
+ <div class="divider"></div>
978
+ <button class="format-btn" id="link-btn" title="Insert Link (Cmd+K)">
979
+ <svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
980
+ </button>
981
+ <div class="divider"></div>
982
+ <div class="format-dropdown" id="insert-dropdown">
983
+ <button class="format-dropdown-btn">
984
+ <span>Insert</span>
985
+ <svg viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"></polyline></svg>
986
+ </button>
987
+ <div class="format-dropdown-menu">
988
+ <button class="format-dropdown-item" data-insert="code">Code Block</button>
989
+ <button class="format-dropdown-item" data-insert="hr">Horizontal Rule</button>
990
+ <button class="format-dropdown-item" data-insert="table">Table</button>
991
+ </div>
992
+ </div>
993
+ <div class="divider"></div>
994
+ <div class="width-toggle" id="width-toggle">
995
+ <button class="width-btn active" data-width="fixed" title="Fixed width">Fixed</button>
996
+ <button class="width-btn" data-width="full" title="Full width">Full</button>
997
+ </div>
998
+ <button class="focus-btn" id="focus-btn" title="Focus mode (Esc to exit)">
999
+ <svg class="focus-icon" viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path></svg>
1000
+ <svg class="exit-icon" viewBox="0 0 24 24"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"></path></svg>
1001
+ <span class="focus-text">Focus</span>
1002
+ <span class="exit-text">Exit</span>
1003
+ </button>
1004
+ <div class="word-count" id="word-count">0 words</div>
1005
+ </div>
664
1006
  <div class="viewer-body">
665
1007
  <section class="document">
666
1008
  <div class="doc-header">
@@ -730,6 +1072,15 @@
730
1072
  const docTitleEl = document.getElementById("doc-title");
731
1073
  const docPathEl = document.getElementById("doc-path");
732
1074
  const editBadgeEl = document.getElementById("edit-badge");
1075
+ const viewerBodyEl = document.querySelector(".viewer-body");
1076
+ const formatToolbarEl = document.getElementById("format-toolbar");
1077
+ const wordCountEl = document.getElementById("word-count");
1078
+ const headingDropdown = document.getElementById("heading-dropdown");
1079
+ const headingLabel = document.getElementById("heading-label");
1080
+ const insertDropdown = document.getElementById("insert-dropdown");
1081
+ const linkBtn = document.getElementById("link-btn");
1082
+ const widthToggle = document.getElementById("width-toggle");
1083
+ const focusBtn = document.getElementById("focus-btn");
733
1084
 
734
1085
  let tocTimer = null;
735
1086
  let scrollRaf = null;
@@ -876,6 +1227,8 @@
876
1227
  editToggleEl.classList.add("hidden");
877
1228
  saveEl.classList.remove("hidden");
878
1229
  cancelEl.classList.remove("hidden");
1230
+ updateWordCount();
1231
+ headingLabel.textContent = "Body";
879
1232
  } else {
880
1233
  contentEl.contentEditable = "false";
881
1234
  contentEl.classList.remove("editing");
@@ -885,6 +1238,13 @@
885
1238
  }
886
1239
  }
887
1240
 
1241
+ function updateWordCount() {
1242
+ const text = contentEl.innerText || "";
1243
+ const words = text.trim().split(/\s+/).filter(w => w.length > 0);
1244
+ const count = words.length;
1245
+ wordCountEl.textContent = `${count} ${count === 1 ? 'word' : 'words'}`;
1246
+ }
1247
+
888
1248
  async function saveFile() {
889
1249
  if (!state.selectedId) return;
890
1250
  const html = contentEl.innerHTML;
@@ -950,10 +1310,12 @@
950
1310
 
951
1311
  function updateActiveFromScroll() {
952
1312
  if (!state.tocHeadings || state.tocHeadings.length === 0) return;
953
- const scrollTop = contentEl.scrollTop;
1313
+ const scrollTop = viewerBodyEl.scrollTop;
1314
+ const containerTop = viewerBodyEl.getBoundingClientRect().top;
954
1315
  let current = state.tocHeadings[0];
955
1316
  for (const heading of state.tocHeadings) {
956
- if (heading.offsetTop - 24 <= scrollTop) {
1317
+ const headingTop = heading.getBoundingClientRect().top - containerTop;
1318
+ if (headingTop <= 50) {
957
1319
  current = heading;
958
1320
  } else {
959
1321
  break;
@@ -1034,10 +1396,11 @@
1034
1396
  contentEl.addEventListener("input", () => {
1035
1397
  if (state.isEditing) {
1036
1398
  scheduleTocRefresh();
1399
+ updateWordCount();
1037
1400
  }
1038
1401
  });
1039
1402
 
1040
- contentEl.addEventListener("scroll", handleContentScroll);
1403
+ viewerBodyEl.addEventListener("scroll", handleContentScroll);
1041
1404
 
1042
1405
  window.addEventListener("resize", () => {
1043
1406
  updateActiveFromScroll();
@@ -1062,6 +1425,206 @@
1062
1425
  // Initialize theme buttons
1063
1426
  updateThemeButtons(getStoredTheme());
1064
1427
 
1428
+ // Formatting toolbar functions
1429
+ function execFormat(cmd, value = null) {
1430
+ contentEl.focus();
1431
+ document.execCommand(cmd, false, value);
1432
+ scheduleTocRefresh();
1433
+ updateWordCount();
1434
+ }
1435
+
1436
+ function formatHeading(tag) {
1437
+ contentEl.focus();
1438
+ if (tag === 'p') {
1439
+ document.execCommand('formatBlock', false, 'p');
1440
+ headingLabel.textContent = 'Body';
1441
+ } else {
1442
+ document.execCommand('formatBlock', false, tag);
1443
+ headingLabel.textContent = tag.toUpperCase();
1444
+ }
1445
+ scheduleTocRefresh();
1446
+ }
1447
+
1448
+ function insertLink() {
1449
+ const url = prompt('Enter URL:', 'https://');
1450
+ if (url) {
1451
+ document.execCommand('createLink', false, url);
1452
+ }
1453
+ }
1454
+
1455
+ function insertCodeBlock() {
1456
+ const code = document.createElement('pre');
1457
+ const codeInner = document.createElement('code');
1458
+ codeInner.textContent = 'code here';
1459
+ code.appendChild(codeInner);
1460
+
1461
+ const selection = window.getSelection();
1462
+ if (selection.rangeCount > 0) {
1463
+ const range = selection.getRangeAt(0);
1464
+ range.deleteContents();
1465
+ range.insertNode(code);
1466
+ range.setStartAfter(code);
1467
+ range.collapse(true);
1468
+ selection.removeAllRanges();
1469
+ selection.addRange(range);
1470
+ }
1471
+ }
1472
+
1473
+ function insertHorizontalRule() {
1474
+ document.execCommand('insertHorizontalRule', false, null);
1475
+ }
1476
+
1477
+ function insertTable() {
1478
+ const table = document.createElement('table');
1479
+ const thead = document.createElement('thead');
1480
+ const tbody = document.createElement('tbody');
1481
+ const headerRow = document.createElement('tr');
1482
+ const bodyRow = document.createElement('tr');
1483
+
1484
+ for (let i = 0; i < 3; i++) {
1485
+ const th = document.createElement('th');
1486
+ th.textContent = `Header ${i + 1}`;
1487
+ headerRow.appendChild(th);
1488
+
1489
+ const td = document.createElement('td');
1490
+ td.textContent = `Cell ${i + 1}`;
1491
+ bodyRow.appendChild(td);
1492
+ }
1493
+
1494
+ thead.appendChild(headerRow);
1495
+ tbody.appendChild(bodyRow);
1496
+ table.appendChild(thead);
1497
+ table.appendChild(tbody);
1498
+
1499
+ const selection = window.getSelection();
1500
+ if (selection.rangeCount > 0) {
1501
+ const range = selection.getRangeAt(0);
1502
+ range.deleteContents();
1503
+ range.insertNode(table);
1504
+ range.setStartAfter(table);
1505
+ range.collapse(true);
1506
+ selection.removeAllRanges();
1507
+ selection.addRange(range);
1508
+ }
1509
+ }
1510
+
1511
+ function closeAllDropdowns() {
1512
+ document.querySelectorAll('.format-dropdown').forEach(d => d.classList.remove('open'));
1513
+ }
1514
+
1515
+ // Format button clicks
1516
+ formatToolbarEl.querySelectorAll('.format-btn[data-cmd]').forEach(btn => {
1517
+ btn.addEventListener('click', (e) => {
1518
+ e.preventDefault();
1519
+ const cmd = btn.dataset.cmd;
1520
+ const value = btn.dataset.value || null;
1521
+ execFormat(cmd, value);
1522
+ });
1523
+ });
1524
+
1525
+ // Heading dropdown
1526
+ headingDropdown.querySelector('.format-dropdown-btn').addEventListener('click', (e) => {
1527
+ e.stopPropagation();
1528
+ closeAllDropdowns();
1529
+ headingDropdown.classList.toggle('open');
1530
+ });
1531
+
1532
+ headingDropdown.querySelectorAll('.format-dropdown-item').forEach(item => {
1533
+ item.addEventListener('click', () => {
1534
+ formatHeading(item.dataset.heading);
1535
+ closeAllDropdowns();
1536
+ });
1537
+ });
1538
+
1539
+ // Insert dropdown
1540
+ insertDropdown.querySelector('.format-dropdown-btn').addEventListener('click', (e) => {
1541
+ e.stopPropagation();
1542
+ closeAllDropdowns();
1543
+ insertDropdown.classList.toggle('open');
1544
+ });
1545
+
1546
+ insertDropdown.querySelectorAll('.format-dropdown-item').forEach(item => {
1547
+ item.addEventListener('click', () => {
1548
+ const insert = item.dataset.insert;
1549
+ if (insert === 'code') insertCodeBlock();
1550
+ else if (insert === 'hr') insertHorizontalRule();
1551
+ else if (insert === 'table') insertTable();
1552
+ closeAllDropdowns();
1553
+ scheduleTocRefresh();
1554
+ });
1555
+ });
1556
+
1557
+ // Link button
1558
+ linkBtn.addEventListener('click', () => {
1559
+ insertLink();
1560
+ });
1561
+
1562
+ // Close dropdowns when clicking outside
1563
+ document.addEventListener('click', () => {
1564
+ closeAllDropdowns();
1565
+ });
1566
+
1567
+ // Keyboard shortcuts for formatting
1568
+ contentEl.addEventListener('keydown', (event) => {
1569
+ if (!state.isEditing) return;
1570
+
1571
+ if (event.metaKey || event.ctrlKey) {
1572
+ switch (event.key.toLowerCase()) {
1573
+ case 'b':
1574
+ event.preventDefault();
1575
+ execFormat('bold');
1576
+ break;
1577
+ case 'i':
1578
+ event.preventDefault();
1579
+ execFormat('italic');
1580
+ break;
1581
+ case 'k':
1582
+ event.preventDefault();
1583
+ insertLink();
1584
+ break;
1585
+ }
1586
+ }
1587
+ });
1588
+
1589
+ // Width toggle
1590
+ function setContentWidth(mode) {
1591
+ contentEl.classList.remove('fixed-width', 'full-width');
1592
+ contentEl.classList.add(mode + '-width');
1593
+ widthToggle.querySelectorAll('.width-btn').forEach(btn => {
1594
+ btn.classList.toggle('active', btn.dataset.width === mode);
1595
+ });
1596
+ localStorage.setItem('skimmd-width', mode);
1597
+ }
1598
+
1599
+ widthToggle.querySelectorAll('.width-btn').forEach(btn => {
1600
+ btn.addEventListener('click', () => {
1601
+ setContentWidth(btn.dataset.width);
1602
+ });
1603
+ });
1604
+
1605
+ // Initialize width from localStorage
1606
+ const savedWidth = localStorage.getItem('skimmd-width') || 'fixed';
1607
+ setContentWidth(savedWidth);
1608
+
1609
+ // Focus mode
1610
+ function toggleFocusMode() {
1611
+ const isEnabled = document.body.classList.toggle('focus-mode');
1612
+ if (isEnabled) {
1613
+ contentEl.focus();
1614
+ }
1615
+ }
1616
+
1617
+ focusBtn.addEventListener('click', () => {
1618
+ toggleFocusMode();
1619
+ });
1620
+
1621
+ // Escape key to exit focus mode
1622
+ document.addEventListener('keydown', (event) => {
1623
+ if (event.key === 'Escape' && document.body.classList.contains('focus-mode')) {
1624
+ document.body.classList.remove('focus-mode');
1625
+ }
1626
+ });
1627
+
1065
1628
  // Live reload via Server-Sent Events
1066
1629
  function setupLiveReload() {
1067
1630
  const eventSource = new EventSource('/api/events');