sdocs-dev 1.0.0 → 1.1.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/bin/sdocs-dev.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * sdoc CLI
4
4
  * Usage:
5
5
  * sdoc report.md # open file in browser
6
- * sdoc share report.md # print shareable URL to stdout
6
+ * sdoc share report.md # copy shareable link to clipboard
7
7
  * sdoc new # blank document in write mode
8
8
  * cat file.md | sdoc # pipe markdown to browser
9
9
  * sdoc # open studio with empty editor
@@ -29,14 +29,14 @@ USAGE
29
29
  sdoc <file> --style Open with style panel
30
30
  sdoc <file> --raw Open raw markdown source
31
31
  sdoc new New blank document (write mode)
32
- sdoc share <file> Print shareable URL to stdout
33
- sdoc share <file> --section "X" URL with section anchor
32
+ sdoc share <file> Copy shareable link to clipboard
33
+ sdoc share <file> --section "X" Link with section anchor
34
34
  sdoc schema Print the full styles schema
35
35
  sdoc defaults Show ~/.sdocs/styles.yaml
36
36
  sdoc defaults --reset Remove default styles
37
37
  sdoc help Show this help
38
38
  cat file.md | sdoc Pipe markdown from stdin
39
- cat file.md | sdoc share Pipe to shareable URL
39
+ cat file.md | sdoc share Pipe to clipboard link
40
40
 
41
41
  MODE FLAGS
42
42
  --read Clean reading view (default when file given)
@@ -83,6 +83,8 @@ GENERAL
83
83
  Default: "Inter"
84
84
  baseFontSize number Base font size in px. All rem/em values scale from this.
85
85
  Default: 16
86
+ background string Page background color (hex).
87
+ Default: "#ffffff" (light) / "#1c1a17" (dark)
86
88
  color string Master body text color (hex). Cascades to headings,
87
89
  paragraphs, and lists unless those are overridden.
88
90
  Default: "#1c1917"
@@ -141,6 +143,30 @@ COLOR CASCADE
141
143
  color → p.color → list.color
142
144
  Set a child color only when you want it to differ from its parent.
143
145
 
146
+ THEME COLORS
147
+ Colors can be set per-theme using \`light:\` and \`dark:\` sub-blocks under \`styles:\`.
148
+ Non-color properties (fonts, sizes, spacing, weights) remain at the top level
149
+ and are shared across both themes.
150
+
151
+ ---
152
+ styles:
153
+ fontFamily: Lora
154
+ baseFontSize: 17
155
+ light:
156
+ background: "#ffffff"
157
+ color: "#1a1a2e"
158
+ h1: { color: "#c0392b" }
159
+ link: { color: "#2563eb" }
160
+ dark:
161
+ background: "#1c1a17"
162
+ color: "#e7e5e2"
163
+ h1: { color: "#ef6f5e" }
164
+ link: { color: "#60a5fa" }
165
+ ---
166
+
167
+ Top-level colors (old format) are treated as light-theme colors for
168
+ backwards compatibility.
169
+
144
170
  FONTS (24 supported, loaded lazily from Google Fonts)
145
171
  Inter · Roboto · Open Sans · Lato · Montserrat · Source Sans 3
146
172
  Oswald · Raleway · Poppins · Merriweather · Ubuntu · Nunito
@@ -152,15 +178,28 @@ EXAMPLE — editorial article with colored heading tiers
152
178
  styles:
153
179
  fontFamily: Lora
154
180
  baseFontSize: 17
155
- color: "#1a1a2e"
156
- headers:
157
- color: "#2c3e50"
158
- h1: { fontSize: 2.3, color: "#c0392b", fontWeight: 700 }
159
- h2: { fontSize: 1.55, color: "#8e44ad", fontWeight: 600 }
160
- h3: { fontSize: 1.2, color: "#16a085", fontWeight: 600 }
181
+ h1: { fontSize: 2.3, fontWeight: 700 }
182
+ h2: { fontSize: 1.55, fontWeight: 600 }
183
+ h3: { fontSize: 1.2, fontWeight: 600 }
161
184
  p: { lineHeight: 1.9, marginBottom: 1.2 }
162
- link: { color: "#e67e22", decoration: "underline" }
163
- blockquote: { borderColor: "#c0392b", borderWidth: 4, color: "#7f8c8d" }
185
+ light:
186
+ background: "#fffaf5"
187
+ color: "#1a1a2e"
188
+ headers: { color: "#2c3e50" }
189
+ h1: { color: "#c0392b" }
190
+ h2: { color: "#8e44ad" }
191
+ h3: { color: "#16a085" }
192
+ link: { color: "#e67e22", decoration: "underline" }
193
+ blockquote: { borderColor: "#c0392b", borderWidth: 4, color: "#7f8c8d" }
194
+ dark:
195
+ background: "#1a1520"
196
+ color: "#e7e5e2"
197
+ headers: { color: "#b0aaa5" }
198
+ h1: { color: "#ef6f5e" }
199
+ h2: { color: "#c490e4" }
200
+ h3: { color: "#5ed4b8" }
201
+ link: { color: "#f0a860", decoration: "underline" }
202
+ blockquote: { borderColor: "#ef6f5e", borderWidth: 4, color: "#9e9590" }
164
203
  ---
165
204
  `;
166
205
 
@@ -310,13 +349,23 @@ function resetDefaults() {
310
349
  }
311
350
 
312
351
  // Deep merge: defaults under file styles (file wins on conflict)
352
+ // Recursive for light:/dark: sub-objects that contain nested objects
313
353
  function mergeStyles(defaults, fileStyles) {
314
354
  if (!defaults) return fileStyles || {};
315
355
  if (!fileStyles) return { ...defaults };
316
356
  const merged = { ...defaults };
317
357
  for (const [k, v] of Object.entries(fileStyles)) {
318
358
  if (typeof v === 'object' && v !== null && typeof merged[k] === 'object' && merged[k] !== null) {
319
- merged[k] = { ...merged[k], ...v };
359
+ // Recurse one level deeper for light/dark blocks that contain nested objects (e.g. h1: { color: ... })
360
+ const inner = { ...merged[k] };
361
+ for (const [ik, iv] of Object.entries(v)) {
362
+ if (typeof iv === 'object' && iv !== null && typeof inner[ik] === 'object' && inner[ik] !== null) {
363
+ inner[ik] = { ...inner[ik], ...iv };
364
+ } else {
365
+ inner[ik] = iv;
366
+ }
367
+ }
368
+ merged[k] = inner;
320
369
  } else {
321
370
  merged[k] = v;
322
371
  }
@@ -412,9 +461,18 @@ if (require.main === module) {
412
461
  section: opts.section,
413
462
  });
414
463
 
415
- // Share: print URL to stdout, don't open browser
464
+ // Share: copy to clipboard
416
465
  if (opts.subcommand === 'share') {
417
- process.stdout.write(url + '\n');
466
+ try {
467
+ const clip = process.platform === 'darwin' ? 'pbcopy'
468
+ : execSync('which xclip 2>/dev/null', { encoding: 'utf-8' }).trim() ? 'xclip -selection clipboard'
469
+ : 'xsel --clipboard --input';
470
+ execSync(clip, { input: url, stdio: ['pipe', 'ignore', 'ignore'] });
471
+ const name = opts.file ? path.basename(opts.file) : 'stdin';
472
+ console.log(`\u2713 Link for ${name} copied to clipboard`);
473
+ } catch (_) {
474
+ process.stdout.write(url + '\n');
475
+ }
418
476
  process.exit(0);
419
477
  }
420
478
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdocs-dev",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Open, share, and style markdown files from the terminal",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -185,6 +185,7 @@ body {
185
185
  flex: 1;
186
186
  overflow: auto;
187
187
  position: relative;
188
+ background-color: var(--bg);
188
189
  }
189
190
 
190
191
  /* drag overlay */
@@ -38,6 +38,51 @@ body.style-mode #right {
38
38
  flex-shrink: 0;
39
39
  }
40
40
 
41
+ /* ── Theme tabs ── */
42
+ #theme-tabs {
43
+ margin-bottom: 10px;
44
+ }
45
+ #theme-tabs .toggle-group {
46
+ display: flex;
47
+ width: 100%;
48
+ gap: 0;
49
+ background: var(--bg-surface);
50
+ border: 1px solid var(--border);
51
+ border-radius: var(--radius);
52
+ overflow: hidden;
53
+ }
54
+ #theme-tabs .btn {
55
+ flex: 1;
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: center;
59
+ gap: 4px;
60
+ padding: 5px 0;
61
+ border: none;
62
+ background: none;
63
+ font-family: var(--font-ui);
64
+ font-size: 11.5px;
65
+ font-weight: 500;
66
+ color: var(--text-3);
67
+ cursor: pointer;
68
+ transition: background .12s, color .12s;
69
+ }
70
+ #theme-tabs .btn:hover {
71
+ background: var(--bg-hover);
72
+ color: var(--text-2);
73
+ }
74
+ #theme-tabs .btn.active {
75
+ background: var(--bg-panel);
76
+ color: var(--text);
77
+ font-weight: 600;
78
+ box-shadow: var(--shadow-sm);
79
+ }
80
+ #theme-tabs .btn svg {
81
+ width: 13px;
82
+ height: 13px;
83
+ flex-shrink: 0;
84
+ }
85
+
41
86
  /* ── Panel sections ── */
42
87
  .panel-section { border-bottom: 1px solid var(--border-subtle); }
43
88
 
@@ -6,6 +6,7 @@
6
6
  max-width: 760px;
7
7
  margin: 0 auto;
8
8
 
9
+ --md-bg: #FFFFFF;
9
10
  --md-font-family: 'Inter', system-ui, sans-serif;
10
11
  --md-base-size: 16px;
11
12
  --md-line-height: 1.75;
@@ -55,6 +56,7 @@
55
56
  font-size: var(--md-base-size);
56
57
  color: var(--md-color);
57
58
  line-height: var(--md-line-height);
59
+ background-color: var(--md-bg);
58
60
  }
59
61
  :is(#rendered, #write) h1 {
60
62
  font-family: var(--md-h-font-family);
@@ -130,9 +132,13 @@
130
132
  #rendered .section-toggle:hover {
131
133
  color: var(--text-2);
132
134
  }
133
- #rendered .md-section-body:not(.open) > :not(.md-section) {
135
+ #rendered .md-section-body:not(.open):not(:has(.md-section-body.open)) > :not(.md-section) {
134
136
  display: none;
135
137
  }
138
+ #rendered .md-section-body:not(.open):has(.md-section-body.open) > :not(.md-section) {
139
+ opacity: 0.55;
140
+ padding-left: 1.2em;
141
+ }
136
142
  #rendered .md-section-body:not(.open) > .md-section:not(:has(> .md-section-body.open)) {
137
143
  padding-left: 1.2em;
138
144
  }
@@ -84,6 +84,7 @@ html[data-theme="dark"] {
84
84
  --md-copy-btn-hover: rgba(255,255,255,0.06);
85
85
  }
86
86
  html[data-theme="dark"] :is(#rendered, #write) {
87
+ --md-bg: #1c1a17;
87
88
  --md-color: #e7e5e2;
88
89
  --md-code-bg: #1a1816;
89
90
  --md-code-color: #b8a99a;
package/public/default.md CHANGED
@@ -18,7 +18,7 @@ styles:
18
18
 
19
19
  If you're working with agents, a document written in markdown is <ins>officially</ins>* 407 times more useful than a document locked inside a `.docx` or `.gdoc` file format. Because of this, I believe Word and GDocs' days are numbered. (*I am the official.)
20
20
 
21
- However, all is not well. While markdown is great for agents, it's a bit annoying for humans. Quickly and elegantly reading a `.md` file requires you to open your code editor and enter "preview" mode. Sharing a markdown file requires you to actually send the file to someone. They then have to download it and find the least annoying way to read it.
21
+ But while markdown is great for agents, it's a bit annoying for humans. Quickly and elegantly reading a `.md` file requires you to open your code editor and enter "preview" mode. Sharing a markdown file requires you to actually send the file to someone. They then have to download it and find the least annoying way to read it.
22
22
 
23
23
  SmallDocs is an [open source](https://github.com/JoshInLisbon/SDocs) attempt at something different. It lets you (or your agent) easily, elegantly and privately **read**, **share**, **format** and **export** `.md` files.
24
24
 
@@ -51,12 +51,23 @@ SDocs uses a `styles:` key with CSS properties written beneath it in YAML:
51
51
  styles:
52
52
  fontFamily: Lora
53
53
  baseFontSize: 17
54
- color: "#1a1a2e"
55
- h1: { fontSize: 2.3, color: "#c0392b", fontWeight: 700 }
54
+ h1: { fontSize: 2.3, fontWeight: 700 }
56
55
  p: { lineHeight: 1.9, marginBottom: 1.2 }
56
+ light:
57
+ background: "#fffaf5"
58
+ color: "#1a1a2e"
59
+ h1: { color: "#c0392b" }
60
+ dark:
61
+ background: "#1a1520"
62
+ color: "#e7e5e2"
63
+ h1: { color: "#ef6f5e" }
57
64
  ---
58
65
  ```
59
66
 
67
+ Non-color properties (fonts, sizes, spacing) are shared across themes and live at the top level. Colors live inside `light:` and `dark:` blocks so both themes render correctly.
68
+
69
+ All color controls are in the **Colors** section of the style panel. The light/dark toggle at the top of that section lets you customize each theme independently. Colors cascade from general to specific — set `color` once and it flows to headings, paragraphs, and lists unless you override them individually.
70
+
60
71
  (Click "**Raw**" — top left — to see the front matter for this file. See all available properties [here](https://sdocs.dev) or by running `npm i sdocs-dev; sdoc schema`.)
61
72
 
62
73
  When a `Styled .md` file is rendered in the SmallDocs interface the specified styles are applied. If a plain `.md` file is rendered the default styles are applied.
@@ -112,6 +123,92 @@ SmallDocs can export your document in four formats:
112
123
  - **Word (.docx)** — a styled Word document generated from the rendered HTML.
113
124
  - **Styled .md** — your markdown with the `styles:` front matter block included. This is the format SmallDocs reads back in, so your formatting is preserved.
114
125
 
126
+ ## The CLI
127
+
128
+ SmallDocs has a command-line tool that lets you open, share, and style markdown files from the terminal. Install it once:
129
+
130
+ ```
131
+ npm i -g sdocs-dev
132
+ ```
133
+
134
+ This gives you the `sdoc` command.
135
+
136
+ ### Open a file
137
+
138
+ ```
139
+ sdoc README.md
140
+ ```
141
+
142
+ Your browser opens with the document styled and readable. That's it — one command to go from `.md` file to formatted document.
143
+
144
+ ### Modes
145
+
146
+ By default, files open in read mode. You can open in any mode:
147
+
148
+ ```
149
+ sdoc README.md # read mode (default)
150
+ sdoc README.md --write # write mode (contentEditable editor)
151
+ sdoc README.md --style # style mode (styling panel visible)
152
+ sdoc README.md --raw # raw mode (plain markdown source)
153
+ ```
154
+
155
+ ### Share a link
156
+
157
+ ```
158
+ sdoc share README.md
159
+ ```
160
+
161
+ This copies a shareable link to your clipboard — no browser opens, no URL printed. The entire document is compressed into the URL hash, so there's nothing to host or upload. You can also combine it with options:
162
+
163
+ ```
164
+ sdoc share report.md --section "Results" # deep-link to a heading
165
+ sdoc share notes.md --write # link opens in write mode
166
+ ```
167
+
168
+ ### Pipe from stdin
169
+
170
+ Any command that outputs markdown can be piped directly into SmallDocs:
171
+
172
+ ```
173
+ cat notes.md | sdoc # open in browser
174
+ cat notes.md | sdoc share # pipe to clipboard link
175
+ your-agent --output md | sdoc # pipe agent output to browser
176
+ ```
177
+
178
+ ### Start a new document
179
+
180
+ ```
181
+ sdoc new
182
+ ```
183
+
184
+ Opens a blank document in write mode, ready to type.
185
+
186
+ ### Default styles
187
+
188
+ If you find a style you like, use the "Save as Default" panel in the Style view to generate a command that saves your preferences to `~/.sdocs/styles.yaml`. The CLI automatically applies these defaults to every file you open — unless the file has its own styles, which always take priority.
189
+
190
+ ```
191
+ sdoc defaults # view your current defaults
192
+ sdoc defaults --reset # remove them
193
+ ```
194
+
195
+ ### Style schema
196
+
197
+ ```
198
+ sdoc schema
199
+ ```
200
+
201
+ Prints every available style property with its type, default value, and description. This is designed to be readable by both humans and LLMs — so your agent can write YAML front matter for you.
202
+
203
+ ### For agents
204
+
205
+ The CLI is designed to work well in automated workflows. A few patterns:
206
+
207
+ - **Generate a styled doc**: have your agent write a `.md` file with YAML front matter, then `sdoc share file.md` to copy a shareable link
208
+ - **Learn the format**: `sdoc schema` gives your agent everything it needs to know about available style properties
209
+ - **Deep-link to context**: `sdoc share file.md --section "Heading"` creates a URL that scrolls straight to the relevant section
210
+ - **No auth, no API keys**: everything is client-side — the URL *is* the document
211
+
115
212
  ### Small opinionated things
116
213
 
117
214
  SmallDocs has opinions. We do some things which might not work for everyone but hopefully make the general `.md` experience better for most.
package/public/index.html CHANGED
@@ -145,11 +145,112 @@
145
145
  <input type="range" id="ctrl-line-height-range" min="1.0" max="2.5" step="0.05" value="1.75">
146
146
  <input type="number" id="ctrl-line-height-num" min="1.0" max="2.5" step="0.05" value="1.75">
147
147
  </div>
148
+ <button class="factory-reset-btn" id="factory-reset-styles" title="Reset all styles to defaults">Reset all styles</button>
149
+ </div>
150
+ </div>
151
+
152
+ <!-- COLORS -->
153
+ <div class="panel-section">
154
+ <div class="panel-header" data-target="body-colors">
155
+ <span class="arrow">
156
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M3 2l4 3-4 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
157
+ </span>
158
+ Colors
159
+ </div>
160
+ <div class="panel-body" id="body-colors">
161
+ <div id="theme-tabs">
162
+ <div class="toggle-group">
163
+ <button class="btn active" id="theme-tab-light">
164
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
165
+ Light
166
+ </button>
167
+ <button class="btn" id="theme-tab-dark">
168
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>
169
+ Dark
170
+ </button>
171
+ </div>
172
+ </div>
173
+ <div class="control-row">
174
+ <label>Background</label>
175
+ <div class="color-pair"><input type="color" id="ctrl-bg-color" value="#ffffff"><button class="reset-btn" id="reset-bg-color" title="Reset to default">↺</button></div>
176
+ </div>
148
177
  <div class="control-row">
149
- <label>Text color</label>
178
+ <label>Text</label>
150
179
  <div class="color-pair"><input type="color" id="ctrl-color" value="#1c1917"><button class="reset-btn" id="reset-color" title="Reset to default">↺</button></div>
151
180
  </div>
152
- <button class="factory-reset-btn" id="factory-reset-styles" title="Reset all styles to defaults">Reset all styles</button>
181
+
182
+ <div class="sub-section">
183
+ <div class="sub-header" data-target="sub-colors-headings">
184
+ <span class="arrow">▶</span> Headings
185
+ </div>
186
+ <div class="sub-body" id="sub-colors-headings">
187
+ <div class="control-row">
188
+ <label>All</label>
189
+ <div class="color-pair"><input type="color" id="ctrl-h-color" value="#1c1917"><button class="reset-btn" id="reset-h-color" title="Follow body text color">↺</button></div>
190
+ </div>
191
+ <div class="control-row">
192
+ <label>H1</label>
193
+ <div class="color-pair"><input type="color" id="ctrl-h1-color" value="#1c1917"><button class="reset-btn" id="reset-h1-color" title="Follow header color">↺</button></div>
194
+ </div>
195
+ <div class="control-row">
196
+ <label>H2</label>
197
+ <div class="color-pair"><input type="color" id="ctrl-h2-color" value="#1c1917"><button class="reset-btn" id="reset-h2-color" title="Follow header color">↺</button></div>
198
+ </div>
199
+ <div class="control-row">
200
+ <label>H3</label>
201
+ <div class="color-pair"><input type="color" id="ctrl-h3-color" value="#1c1917"><button class="reset-btn" id="reset-h3-color" title="Follow header color">↺</button></div>
202
+ </div>
203
+ <div class="control-row">
204
+ <label>H4</label>
205
+ <div class="color-pair"><input type="color" id="ctrl-h4-color" value="#1c1917"><button class="reset-btn" id="reset-h4-color" title="Follow header color">↺</button></div>
206
+ </div>
207
+ </div>
208
+ </div>
209
+
210
+ <div class="control-row">
211
+ <label>Paragraph</label>
212
+ <div class="color-pair"><input type="color" id="ctrl-p-color" value="#1c1917"><button class="reset-btn" id="reset-p-color" title="Follow body text color">↺</button></div>
213
+ </div>
214
+ <div class="control-row">
215
+ <label>Lists</label>
216
+ <div class="color-pair"><input type="color" id="ctrl-list-color" value="#1c1917"><button class="reset-btn" id="reset-list-color" title="Follow paragraph color">↺</button></div>
217
+ </div>
218
+ <div class="control-row">
219
+ <label>Link</label>
220
+ <div class="color-pair"><input type="color" id="ctrl-link-color" value="#2563eb"><button class="reset-btn" id="reset-link-color" title="Reset to default">↺</button></div>
221
+ </div>
222
+
223
+ <div class="sub-section">
224
+ <div class="sub-header" data-target="sub-colors-code">
225
+ <span class="arrow">▶</span> Code
226
+ </div>
227
+ <div class="sub-body" id="sub-colors-code">
228
+ <div class="control-row">
229
+ <label>Background</label>
230
+ <div class="color-pair"><input type="color" id="ctrl-code-bg" value="#f4f1ed"><button class="reset-btn" id="reset-code-bg" title="Reset to default">↺</button></div>
231
+ </div>
232
+ <div class="control-row">
233
+ <label>Text</label>
234
+ <div class="color-pair"><input type="color" id="ctrl-code-color" value="#6b21a8"><button class="reset-btn" id="reset-code-color" title="Reset to default">↺</button></div>
235
+ </div>
236
+ </div>
237
+ </div>
238
+
239
+ <div class="sub-section">
240
+ <div class="sub-header" data-target="sub-colors-bq">
241
+ <span class="arrow">▶</span> Blockquote
242
+ </div>
243
+ <div class="sub-body" id="sub-colors-bq">
244
+ <div class="control-row">
245
+ <label>Border</label>
246
+ <div class="color-pair"><input type="color" id="ctrl-bq-border-color" value="#2563eb"><button class="reset-btn" id="reset-bq-border-color" title="Reset to default">↺</button></div>
247
+ </div>
248
+ <div class="control-row">
249
+ <label>Text</label>
250
+ <div class="color-pair"><input type="color" id="ctrl-bq-color" value="#6b6560"><button class="reset-btn" id="reset-bq-color" title="Reset to default">↺</button></div>
251
+ </div>
252
+ </div>
253
+ </div>
153
254
  </div>
154
255
  </div>
155
256
 
@@ -183,10 +284,6 @@
183
284
  <input type="number" id="ctrl-h-mb-num" min="0" max="2" step="0.1" value="0.4">
184
285
  <span class="unit">em</span>
185
286
  </div>
186
- <div class="control-row">
187
- <label>Color</label>
188
- <div class="color-pair"><input type="color" id="ctrl-h-color" value="#1c1917"><button class="reset-btn" id="reset-h-color" title="Follow body text color">↺</button></div>
189
- </div>
190
287
  </div>
191
288
  </div>
192
289
 
@@ -201,10 +298,6 @@
201
298
  <input type="number" id="ctrl-h1-size-num" min="1" max="4" step="0.1" value="2.1">
202
299
  <span class="unit">em</span>
203
300
  </div>
204
- <div class="control-row">
205
- <label>Color</label>
206
- <div class="color-pair"><input type="color" id="ctrl-h1-color" value="#1c1917"><button class="reset-btn" id="reset-h1-color" title="Follow header color">↺</button></div>
207
- </div>
208
301
  <div class="control-row">
209
302
  <label>Weight</label>
210
303
  <select id="ctrl-h1-weight">
@@ -229,10 +322,6 @@
229
322
  <input type="number" id="ctrl-h2-size-num" min="1" max="3" step="0.1" value="1.55">
230
323
  <span class="unit">em</span>
231
324
  </div>
232
- <div class="control-row">
233
- <label>Color</label>
234
- <div class="color-pair"><input type="color" id="ctrl-h2-color" value="#1c1917"><button class="reset-btn" id="reset-h2-color" title="Follow header color">↺</button></div>
235
- </div>
236
325
  <div class="control-row">
237
326
  <label>Weight</label>
238
327
  <select id="ctrl-h2-weight">
@@ -256,10 +345,6 @@
256
345
  <input type="number" id="ctrl-h3-size-num" min="0.8" max="2.5" step="0.1" value="1.2">
257
346
  <span class="unit">em</span>
258
347
  </div>
259
- <div class="control-row">
260
- <label>Color</label>
261
- <div class="color-pair"><input type="color" id="ctrl-h3-color" value="#1c1917"><button class="reset-btn" id="reset-h3-color" title="Follow header color">↺</button></div>
262
- </div>
263
348
  <div class="control-row">
264
349
  <label>Weight</label>
265
350
  <select id="ctrl-h3-weight">
@@ -283,10 +368,6 @@
283
368
  <input type="number" id="ctrl-h4-size-num" min="0.8" max="2" step="0.1" value="1.0">
284
369
  <span class="unit">em</span>
285
370
  </div>
286
- <div class="control-row">
287
- <label>Color</label>
288
- <div class="color-pair"><input type="color" id="ctrl-h4-color" value="#1c1917"><button class="reset-btn" id="reset-h4-color" title="Follow header color">↺</button></div>
289
- </div>
290
371
  <div class="control-row">
291
372
  <label>Weight</label>
292
373
  <select id="ctrl-h4-weight">
@@ -311,10 +392,6 @@
311
392
  Paragraph
312
393
  </div>
313
394
  <div class="panel-body" id="body-paragraph">
314
- <div class="control-row">
315
- <label>Color</label>
316
- <div class="color-pair"><input type="color" id="ctrl-p-color" value="#1c1917"><button class="reset-btn" id="reset-p-color" title="Follow body text color">↺</button></div>
317
- </div>
318
395
  <div class="control-row">
319
396
  <label>Line height</label>
320
397
  <input type="range" id="ctrl-p-lh-range" min="1.0" max="2.5" step="0.05" value="1.75">
@@ -338,10 +415,6 @@
338
415
  Lists
339
416
  </div>
340
417
  <div class="panel-body" id="body-lists">
341
- <div class="control-row">
342
- <label>Color</label>
343
- <div class="color-pair"><input type="color" id="ctrl-list-color" value="#1c1917"><button class="reset-btn" id="reset-list-color" title="Follow paragraph color">↺</button></div>
344
- </div>
345
418
  <div class="control-row">
346
419
  <label>Item spacing</label>
347
420
  <input type="range" id="ctrl-list-spacing-range" min="0" max="1.5" step="0.05" value="0.3">
@@ -366,10 +439,6 @@
366
439
  Links
367
440
  </div>
368
441
  <div class="panel-body" id="body-links">
369
- <div class="control-row">
370
- <label>Color</label>
371
- <div class="color-pair"><input type="color" id="ctrl-link-color" value="#2563eb"><button class="reset-btn" id="reset-link-color" title="Reset to default">↺</button></div>
372
- </div>
373
442
  <div class="control-row">
374
443
  <label>Decoration</label>
375
444
  <select id="ctrl-link-decoration">
@@ -401,14 +470,6 @@
401
470
  <option value="monospace">System mono</option>
402
471
  </select>
403
472
  </div>
404
- <div class="control-row">
405
- <label>Background</label>
406
- <div class="color-pair"><input type="color" id="ctrl-code-bg" value="#f4f1ed"><button class="reset-btn" id="reset-code-bg" title="Reset to default">↺</button></div>
407
- </div>
408
- <div class="control-row">
409
- <label>Text color</label>
410
- <div class="color-pair"><input type="color" id="ctrl-code-color" value="#6b21a8"><button class="reset-btn" id="reset-code-color" title="Reset to default">↺</button></div>
411
- </div>
412
473
  </div>
413
474
  </div>
414
475
 
@@ -421,10 +482,6 @@
421
482
  Blockquote
422
483
  </div>
423
484
  <div class="panel-body" id="body-blockquote">
424
- <div class="control-row">
425
- <label>Border color</label>
426
- <div class="color-pair"><input type="color" id="ctrl-bq-border-color" value="#2563eb"><button class="reset-btn" id="reset-bq-border-color" title="Reset to default">↺</button></div>
427
- </div>
428
485
  <div class="control-row">
429
486
  <label>Border width</label>
430
487
  <input type="range" id="ctrl-bq-bw-range" min="1" max="8" step="1" value="3">
@@ -437,10 +494,6 @@
437
494
  <input type="number" id="ctrl-bq-size-num" min="0.7" max="1.5" step="0.05" value="1.0">
438
495
  <span class="unit">em</span>
439
496
  </div>
440
- <div class="control-row">
441
- <label>Text color</label>
442
- <div class="color-pair"><input type="color" id="ctrl-bq-color" value="#6b6560"><button class="reset-btn" id="reset-bq-color" title="Reset to default">↺</button></div>
443
- </div>
444
497
  </div>
445
498
  </div>
446
499
 
@@ -147,6 +147,7 @@ function render() {
147
147
  S.renderedEl.querySelectorAll('.md-section > h2, .md-section > h3, .md-section > h4').forEach(function(heading) {
148
148
  heading.addEventListener('click', function(e) {
149
149
  if (e.target.closest('.header-anchor') || e.target.closest('.header-copy-btn')) return;
150
+ var yBefore = heading.getBoundingClientRect().top;
150
151
  var section = heading.closest('.md-section');
151
152
  var body = section.querySelector('.md-section-body');
152
153
  var toggle = section.querySelector('.section-toggle');
@@ -154,6 +155,10 @@ function render() {
154
155
  toggle.classList.toggle('open', isOpen);
155
156
  body.querySelectorAll('.md-section-body').forEach(function(b) { b.classList.toggle('open', isOpen); });
156
157
  body.querySelectorAll('.section-toggle').forEach(function(t) { t.classList.toggle('open', isOpen); });
158
+ var yAfter = heading.getBoundingClientRect().top;
159
+ if (yAfter !== yBefore) {
160
+ contentArea.scrollTop += yAfter - yBefore;
161
+ }
157
162
  });
158
163
  });
159
164
  }
@@ -366,6 +371,8 @@ function setMode(mode, skipHash) {
366
371
  }
367
372
 
368
373
  document.getElementById('btn-theme').addEventListener('click', function() { S.toggleTheme(); });
374
+ document.getElementById('theme-tab-light').addEventListener('click', function() { S.switchThemeAndUpdate('light'); });
375
+ document.getElementById('theme-tab-dark').addEventListener('click', function() { S.switchThemeAndUpdate('dark'); });
369
376
  document.getElementById('btn-read').addEventListener('click', function() { setMode('read'); });
370
377
  document.getElementById('btn-style').addEventListener('click', function() { setMode('style'); });
371
378
  document.getElementById('btn-write').addEventListener('click', function() { setMode('write'); });
@@ -466,6 +473,9 @@ S.setMode = setMode;
466
473
  S.render = render;
467
474
  S.loadText = loadText;
468
475
 
476
+ // Sync theme tabs to initial theme
477
+ S.updateThemeTabs(S.activeTheme);
478
+
469
479
  // ── Init ──────────────────────────────────
470
480
 
471
481
  (async function () {