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 +93 -55
- package/bin/skimmd.js +16 -2
- package/darkMode.png +0 -0
- package/lightMode.png +0 -0
- package/package.json +1 -1
- package/public/index.html +577 -14
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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.
|
|
18
|
-
2.
|
|
19
|
-
3.
|
|
20
|
-
4.
|
|
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
|
+

|
|
40
|
+
|
|
41
|
+
### Light Mode
|
|
42
|
+
|
|
43
|
+

|
|
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
|
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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.
|
|
129
|
-
2.
|
|
130
|
-
3.
|
|
131
|
-
4.
|
|
132
|
-
5.
|
|
133
|
-
6.
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
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:
|
|
251
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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');
|