kash-shell 0.3.17__py3-none-any.whl → 0.3.18__py3-none-any.whl

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.
Files changed (37) hide show
  1. kash/actions/core/minify_html.py +41 -0
  2. kash/commands/base/show_command.py +11 -1
  3. kash/config/colors.py +6 -2
  4. kash/docs/markdown/topics/a1_what_is_kash.md +52 -23
  5. kash/docs/markdown/topics/a2_installation.md +17 -30
  6. kash/docs/markdown/topics/a3_getting_started.md +5 -19
  7. kash/exec/action_exec.py +1 -1
  8. kash/exec/fetch_url_metadata.py +9 -0
  9. kash/exec/precondition_registry.py +3 -3
  10. kash/file_storage/file_store.py +18 -1
  11. kash/llm_utils/llm_features.py +5 -1
  12. kash/llm_utils/llms.py +18 -8
  13. kash/media_base/media_cache.py +48 -24
  14. kash/media_base/media_services.py +63 -14
  15. kash/media_base/services/local_file_media.py +9 -1
  16. kash/model/items_model.py +4 -5
  17. kash/model/media_model.py +9 -1
  18. kash/model/params_model.py +9 -3
  19. kash/utils/common/function_inspect.py +97 -1
  20. kash/utils/common/testing.py +58 -0
  21. kash/utils/common/url_slice.py +329 -0
  22. kash/utils/file_utils/file_formats.py +1 -1
  23. kash/utils/text_handling/markdown_utils.py +424 -16
  24. kash/web_gen/templates/base_styles.css.jinja +137 -15
  25. kash/web_gen/templates/base_webpage.html.jinja +13 -17
  26. kash/web_gen/templates/components/toc_scripts.js.jinja +319 -0
  27. kash/web_gen/templates/components/toc_styles.css.jinja +284 -0
  28. kash/web_gen/templates/components/tooltip_scripts.js.jinja +730 -0
  29. kash/web_gen/templates/components/tooltip_styles.css.jinja +482 -0
  30. kash/web_gen/templates/content_styles.css.jinja +13 -8
  31. kash/web_gen/templates/simple_webpage.html.jinja +15 -481
  32. kash/workspaces/workspaces.py +10 -1
  33. {kash_shell-0.3.17.dist-info → kash_shell-0.3.18.dist-info}/METADATA +75 -72
  34. {kash_shell-0.3.17.dist-info → kash_shell-0.3.18.dist-info}/RECORD +37 -30
  35. {kash_shell-0.3.17.dist-info → kash_shell-0.3.18.dist-info}/WHEEL +0 -0
  36. {kash_shell-0.3.17.dist-info → kash_shell-0.3.18.dist-info}/entry_points.txt +0 -0
  37. {kash_shell-0.3.17.dist-info → kash_shell-0.3.18.dist-info}/licenses/LICENSE +0 -0
@@ -5,7 +5,8 @@
5
5
  --font-sans: "Source Sans 3 Variable", sans-serif, "Hack Nerd Font";
6
6
  --font-serif: "PT Serif", serif, "Hack Nerd Font";
7
7
  /* Source Sans 3 Variable better at these weights. */
8
- --font-weight-sans-bold: 620;
8
+ --font-weight-sans-medium: 565;
9
+ --font-weight-sans-bold: 650;
9
10
  --font-mono: "Hack Nerd Font", "Menlo", "DejaVu Sans Mono", Consolas, "Lucida Console", monospace;
10
11
 
11
12
  --font-size-large: 1.2rem;
@@ -68,6 +69,14 @@
68
69
  }
69
70
  {% endblock scrollbar_styles %}
70
71
 
72
+ {% block html_styles %}
73
+ /* Prevent horizontal overflow at the root level */
74
+ html {
75
+ overflow-x: hidden;
76
+ width: 100%;
77
+ }
78
+ {% endblock html_styles %}
79
+
71
80
  {% block body_styles %}
72
81
  body {
73
82
  font-family: var(--font-serif);
@@ -109,7 +118,9 @@ a:hover {
109
118
  h1,
110
119
  h2,
111
120
  h3,
112
- h4 {
121
+ h4,
122
+ h5,
123
+ h6 {
113
124
  line-height: 1.2;
114
125
  }
115
126
 
@@ -120,7 +131,7 @@ h1 {
120
131
  }
121
132
 
122
133
  h2 {
123
- font-size: 1.4rem;
134
+ font-size: 1.42rem;
124
135
  margin-top: 2rem;
125
136
  margin-bottom: 1rem;
126
137
  }
@@ -129,17 +140,32 @@ h1 + h2 {
129
140
  margin-top: 2rem;
130
141
  }
131
142
 
143
+ h2 + h3 {
144
+ margin-top: 1.1rem;
145
+ }
146
+
132
147
  h3 {
133
- font-size: 1.03rem;
134
- margin-top: 1.6rem;
135
- margin-bottom: 0.5rem;
148
+ font-size: 1.18rem;
149
+ margin-top: 1.4rem;
150
+ margin-bottom: 0.7rem;
136
151
  }
137
152
 
138
153
  h4 {
154
+ font-size: 1.1rem;
139
155
  margin-top: 1rem;
140
156
  margin-bottom: 0.7rem;
141
157
  }
142
158
 
159
+ h5, h6 {
160
+ font-size: 1rem;
161
+ margin-top: 0.7rem;
162
+ margin-bottom: 0.5rem;
163
+ }
164
+
165
+ h4+p, h5+p, h6+p {
166
+ margin-top: 0;
167
+ }
168
+
143
169
  ul {
144
170
  list-style-type: none;
145
171
  margin-left: 1.8rem;
@@ -262,6 +288,44 @@ pre > code {
262
288
  height: 0.875rem;
263
289
  }
264
290
 
291
+ img {
292
+ margin: 1rem 0;
293
+ }
294
+
295
+ details {
296
+ font-family: var(--font-sans);
297
+ color: var(--color-text);
298
+
299
+ border: 1px solid var(--color-hint-gentle);
300
+ border-radius: 3px;
301
+ margin: 0.75rem 0;
302
+ }
303
+
304
+ summary {
305
+ color: var(--color-secondary);
306
+ padding: .5rem 1rem;
307
+ cursor: pointer;
308
+ user-select: none;
309
+ background: var(--color-bg-alt);
310
+ transition: all 0.15s ease-in-out;
311
+ }
312
+
313
+ summary:hover {
314
+ color: var(--color-primary-light);
315
+ {# background: var(--color-hover-bg); #}
316
+ }
317
+
318
+ /* keep the border on the summary when open so it blends */
319
+ details[open] summary {
320
+ border-bottom: 1px solid var(--color-hint-gentle);
321
+ }
322
+ /* focus ring for a11y */
323
+ summary:focus-visible {
324
+ outline: 3px solid var(--color-primary);
325
+ outline-offset: 2px;
326
+ }
327
+
328
+
265
329
  hr {
266
330
  border: none;
267
331
  height: 1.5rem;
@@ -284,6 +348,7 @@ hr:before {
284
348
  {% endblock typography %}
285
349
 
286
350
  {% block long_text_styles %}
351
+
287
352
  /* Long text stylings, for nicely formatting blog post length or longer texts. */
288
353
 
289
354
  .long-text {
@@ -303,23 +368,36 @@ hr:before {
303
368
 
304
369
  .long-text h3 {
305
370
  font-family: var(--font-sans);
306
- font-weight: var(--font-weight-sans-bold);
307
- font-size: 1.05rem;
371
+ font-weight: 565;
308
372
  text-transform: uppercase;
309
373
  letter-spacing: 0.025em;
310
374
  }
311
375
 
312
376
  .long-text h4 {
377
+ font-family: var(--font-sans);
378
+ font-weight: 650;
379
+ letter-spacing: 0.02em;
380
+ }
381
+
382
+ .long-text h5 {
313
383
  font-family: var(--font-serif);
314
384
  font-weight: 700;
315
385
  }
316
386
 
387
+ .long-text h6 {
388
+ font-family: var(--font-serif);
389
+ font-weight: 400;
390
+ font-style: italic;
391
+ }
392
+
317
393
  .subtitle {
318
394
  font-family: var(--font-serif);
319
395
  font-style: italic;
320
396
  font-size: 1rem;
321
397
  }
322
398
 
399
+ /* Adjustments to long text for pure sans-serif pages. */
400
+
323
401
  .long-text .sans-text {
324
402
  font-family: var(--font-sans);
325
403
  }
@@ -347,7 +425,7 @@ hr:before {
347
425
 
348
426
  .long-text .sans-text h3 {
349
427
  font-family: var(--font-sans);
350
- font-size: 1.03rem;
428
+ font-size: 1.1rem;
351
429
  font-weight: var(--font-weight-sans-bold);
352
430
  text-transform: uppercase;
353
431
  letter-spacing: 0.03em;
@@ -370,6 +448,7 @@ table {
370
448
  }
371
449
 
372
450
  th {
451
+ font-weight: var(--font-weight-sans-bold);
373
452
  text-transform: uppercase;
374
453
  letter-spacing: 0.03em;
375
454
  border-bottom: 1px solid var(--color-border-hint);
@@ -399,6 +478,9 @@ tbody tr:nth-child(even) {
399
478
  /* Default: center tables within their container */
400
479
  left: 50%;
401
480
  transform: translateX(-50%);
481
+ /* Prevent container from expanding beyond its content area */
482
+ overflow-x: auto;
483
+ overflow-y: visible;
402
484
  }
403
485
 
404
486
  /* When TOC is present, simplify table container positioning */
@@ -456,6 +538,9 @@ sup {
456
538
  }
457
539
  .table-container {
458
540
  width: calc(100vw - 6rem);
541
+ /* Ensure container doesn't expand beyond its width */
542
+ max-width: calc(100vw - 6rem);
543
+ contain: layout inline-size;
459
544
  }
460
545
 
461
546
  /* Apply shadow to long-text containers on larger screens */
@@ -472,14 +557,46 @@ sup {
472
557
 
473
558
  /* Handle TOC layouts specially - tables should bleed within their grid column */
474
559
  @media (min-width: 1200px) {
475
- /* When TOC is present, tables should bleed within the content grid column */
476
- .content-with-toc.has-toc table {
477
- /* Calculate available width: full viewport minus TOC column width minus margins */
478
- width: calc(100vw - var(--toc-width, 16rem) - 8rem);
479
- max-width: none;
560
+ .content-with-toc.has-toc {
561
+ /* Define reusable values for clarity */
562
+ --content-width: 48rem;
563
+ --content-min-gap: 2rem;
564
+ --table-right-margin: 2rem;
565
+ --long-text-padding: 4rem; /* md:px-16 = 4rem */
566
+
567
+ /* Where content would be if centered in viewport */
568
+ --content-centered-left: calc((100vw - var(--content-width)) / 2);
569
+
570
+ /* Content's left margin within its grid column */
571
+ --content-margin-left: max(var(--content-min-gap), calc(var(--content-centered-left) - var(--toc-width)));
572
+
573
+ /* Content text's actual position from viewport left edge (excluding padding) */
574
+ --content-text-viewport-left: calc(var(--toc-width) + var(--content-margin-left));
480
575
  }
576
+
577
+ /* When TOC is present, tables should align with main content and bleed right */
481
578
  .content-with-toc.has-toc .table-container {
482
- width: calc(100vw - var(--toc-width, 16rem) - 8rem);
579
+ /* Remove default positioning */
580
+ left: 0;
581
+ transform: none;
582
+
583
+ /* Pull table left to align with content's text position */
584
+ /* Need to compensate for both content margin AND long-text padding */
585
+ margin-left: calc(-1 * (var(--content-margin-left) + var(--long-text-padding)));
586
+
587
+ /* Table bleeds wide as can fit */
588
+ width: calc(100vw - var(--content-text-viewport-left) - var(--table-right-margin));
589
+ max-width: calc(100vw - var(--content-text-viewport-left) - var(--table-right-margin));
590
+
591
+ /* Ensure horizontal scroll works properly without expanding container */
592
+ overflow-x: auto;
593
+ overflow-y: visible;
594
+ contain: layout inline-size;
595
+ }
596
+
597
+ .content-with-toc.has-toc table {
598
+ /* Let table fill its container */
599
+ width: 100%;
483
600
  max-width: none;
484
601
  }
485
602
 
@@ -517,13 +634,18 @@ sup {
517
634
 
518
635
  /* Make table containers scrollable without affecting page layout */
519
636
  .table-container {
637
+ width: calc(100vw - 3rem); /* Fixed width instead of max-width */
520
638
  max-width: calc(100vw - 3rem);
521
639
  overflow-x: auto;
640
+ overflow-y: visible; /* Ensure vertical overflow is not hidden */
522
641
  transform: none;
523
642
  left: 0;
524
643
  position: relative;
525
644
  margin-left: auto;
526
645
  margin-right: auto;
646
+ /* Prevent container from expanding beyond its width */
647
+ box-sizing: border-box;
648
+ contain: layout inline-size; /* CSS containment to prevent width expansion */
527
649
  }
528
650
 
529
651
  table {
@@ -50,14 +50,13 @@
50
50
  <link rel="preload" as="font" type="font/woff2" crossorigin
51
51
  href="https://cdn.jsdelivr.net/fontsource/fonts/source-sans-3:vf@latest/latin-wght-italic.woff2" />
52
52
 
53
- <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet" />
53
+ <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
54
54
  <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js" defer></script>
55
55
  {% endblock head_basic %}
56
56
 
57
57
  {% block head_extra %}{% endblock head_extra %}
58
58
 
59
59
  <style>
60
-
61
60
  body {
62
61
  background: var(--color-bg);
63
62
  color: var(--color-text);
@@ -65,7 +64,7 @@
65
64
  }
66
65
 
67
66
  .button {
68
- color: var(--color-hint);
67
+ color: var(--color-hint-strong);
69
68
  background: var(--color-bg);
70
69
  border: none;
71
70
  padding: 0;
@@ -75,8 +74,8 @@
75
74
  display: flex;
76
75
  align-items: center;
77
76
  justify-content: center;
78
- width: 2.5rem;
79
- height: 2.5rem;
77
+ width: 2.2rem;
78
+ height: 2.2rem;
80
79
 
81
80
  /* Separate transitions for theme vs interaction */
82
81
  transition: background-color 0.4s ease-in-out,
@@ -88,23 +87,15 @@
88
87
  .button:hover {
89
88
  background: var(--color-hover-bg);
90
89
  color: var(--color-primary);
90
+ transition: background-color 0.4s ease-in-out, color 0.4s ease-in-out;
91
91
  }
92
92
 
93
93
  .button svg {
94
- width: 1rem;
95
- height: 1rem;
96
- transition: inherit; /* Inherit the color transition */
94
+ width: 1.2rem;
95
+ height: 1.2rem;
96
+ transition: background-color 0.4s ease-in-out, color 0.4s ease-in-out;
97
97
  }
98
98
 
99
- /* Dark mode styles for buttons */
100
- [data-theme="dark"] .button {
101
- color: var(--color-primary-light);
102
- }
103
-
104
- [data-theme="dark"] .button:hover {
105
- background: var(--color-hover-bg);
106
- color: var(--color-bright);
107
- }
108
99
 
109
100
  /* Positioning class for fixed buttons */
110
101
  .fixed-button {
@@ -112,6 +103,11 @@
112
103
  top: 1rem;
113
104
  }
114
105
 
106
+ .floating-button {
107
+ border: 1px solid var(--color-hint-gentle);
108
+ background: var(--color-bg-alt);
109
+ }
110
+
115
111
  /* Specific positioning and z-index for theme toggle */
116
112
  .theme-toggle {
117
113
  right: 1rem;
@@ -0,0 +1,319 @@
1
+ // Table of Contents functionality
2
+ function initTOC() {
3
+ const tocContainer = document.getElementById('toc-container');
4
+ const tocList = document.getElementById('toc-list');
5
+ const tocToggle = document.getElementById('toc-toggle');
6
+ const contentContainer = document.getElementById('content-container');
7
+ const mainContent = document.getElementById('main-content');
8
+
9
+ if (!tocContainer || !tocList || !mainContent) {
10
+ console.debug("TOC not initialized: missing elements");
11
+ return;
12
+ }
13
+
14
+ const tocBreakpoint = parseInt(
15
+ getComputedStyle(document.documentElement)
16
+ .getPropertyValue('--toc-breakpoint')
17
+ .replace('px', '')
18
+ );
19
+
20
+ // Find all headings in the main content
21
+ const headings = mainContent.querySelectorAll('{{ toc_headings | default("h1, h2, h3") }}');
22
+ // Only show TOC if we have toc_min_headings (default 10) or more headings
23
+ const tocThreshold = {{ toc_min_headings | default(10) }};
24
+
25
+ if (headings.length < tocThreshold) {
26
+ // TOC is disabled
27
+ contentContainer.classList.remove('content-with-toc');
28
+ if (tocToggle) {
29
+ tocToggle.style.display = 'none';
30
+ }
31
+ console.debug("TOC hidden: not enough headings");
32
+ return;
33
+ }
34
+
35
+ // TOC is enabled
36
+ contentContainer.classList.add('has-toc'); // This triggers grid layout
37
+ mainContent.classList.add('with-toc');
38
+ document.body.classList.add('page-has-toc');
39
+
40
+ if (tocToggle) {
41
+ tocToggle.style.display = 'flex';
42
+ // Ensure feather icon is rendered after making visible
43
+ if (typeof feather !== 'undefined') {
44
+ feather.replace();
45
+ }
46
+ }
47
+
48
+ // Generate TOC items
49
+ tocList.innerHTML = '';
50
+
51
+ // If there is only one h1, skip it as it is the title of the page.
52
+ let filteredHeadings = Array.from(headings);
53
+ if (headings.length > 0) {
54
+ const firstHeading = headings[0];
55
+ const h1Count = filteredHeadings.filter(h => h.tagName.toLowerCase() === 'h1').length;
56
+
57
+ if (firstHeading.tagName.toLowerCase() === 'h1' && h1Count === 1) {
58
+ filteredHeadings = filteredHeadings.slice(1);
59
+ }
60
+ }
61
+
62
+ filteredHeadings.forEach((heading, index) => {
63
+ // Ensure heading has an ID
64
+ if (!heading.id) {
65
+ const text = heading.textContent.trim().toLowerCase()
66
+ .replace(/[^\w\s-]/g, '')
67
+ .replace(/\s+/g, '-')
68
+ .replace(/-+/g, '-')
69
+ .replace(/^-|-$/g, '');
70
+ heading.id = text || `heading-${index}`;
71
+ }
72
+
73
+ const level = heading.tagName.toLowerCase();
74
+ const text = heading.textContent.trim();
75
+
76
+ const li = document.createElement('li');
77
+ const a = document.createElement('a');
78
+ a.href = `#${heading.id}`;
79
+ a.textContent = text;
80
+ a.className = `toc-link toc-${level}`;
81
+
82
+ li.appendChild(a);
83
+ tocList.appendChild(li);
84
+ });
85
+
86
+ // Mobile TOC toggle functionality
87
+ if (tocToggle) {
88
+ const tocBackdrop = document.getElementById('toc-backdrop');
89
+ let scrollPosition = 0;
90
+
91
+ // Calculate scrollbar width
92
+ const getScrollbarWidth = () => {
93
+ // Create a temporary div with scrollbar
94
+ const outer = document.createElement('div');
95
+ outer.style.visibility = 'hidden';
96
+ outer.style.overflow = 'scroll';
97
+ outer.style.msOverflowStyle = 'scrollbar';
98
+ document.body.appendChild(outer);
99
+
100
+ // Add inner div
101
+ const inner = document.createElement('div');
102
+ outer.appendChild(inner);
103
+
104
+ // Calculate scrollbar width
105
+ const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
106
+
107
+ // Clean up
108
+ outer.parentNode.removeChild(outer);
109
+
110
+ return scrollbarWidth;
111
+ };
112
+
113
+ const openTOC = () => {
114
+ // Save current scroll position
115
+ scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
116
+
117
+ // Calculate scrollbar width and add padding to prevent shift
118
+ const scrollbarWidth = getScrollbarWidth();
119
+ const hasVerticalScrollbar = document.documentElement.scrollHeight > window.innerHeight;
120
+
121
+ // Add classes to show TOC and prevent body scroll
122
+ tocContainer.classList.add('mobile-visible');
123
+ if (tocBackdrop) tocBackdrop.classList.add('visible');
124
+ document.body.classList.add('toc-open');
125
+
126
+ // Set body position to maintain scroll position while fixed
127
+ document.body.style.top = `-${scrollPosition}px`;
128
+
129
+ // Add padding to compensate for scrollbar removal (only if there was a scrollbar)
130
+ if (hasVerticalScrollbar && scrollbarWidth > 0) {
131
+ document.body.style.paddingRight = `${scrollbarWidth}px`;
132
+ }
133
+ };
134
+
135
+ const closeTOC = () => {
136
+ // Remove classes
137
+ tocContainer.classList.remove('mobile-visible');
138
+ if (tocBackdrop) tocBackdrop.classList.remove('visible');
139
+ document.body.classList.remove('toc-open');
140
+
141
+ // Restore body position and scroll
142
+ document.body.style.top = '';
143
+ document.body.style.paddingRight = '';
144
+ window.scrollTo(0, scrollPosition);
145
+ };
146
+
147
+ tocToggle.addEventListener('click', () => {
148
+ if (tocContainer.classList.contains('mobile-visible')) {
149
+ closeTOC();
150
+ } else {
151
+ openTOC();
152
+ }
153
+ });
154
+
155
+ // Close TOC when clicking backdrop
156
+ if (tocBackdrop) {
157
+ tocBackdrop.addEventListener('click', closeTOC);
158
+ }
159
+
160
+ // Update the existing click handler to use closeTOC
161
+ document.addEventListener('click', (e) => {
162
+ if (window.innerWidth < tocBreakpoint &&
163
+ tocContainer.classList.contains('mobile-visible') &&
164
+ !tocContainer.contains(e.target) &&
165
+ !tocToggle.contains(e.target)) {
166
+ closeTOC();
167
+ }
168
+ });
169
+
170
+ // Prevent touch events from propagating through TOC
171
+ tocContainer.addEventListener('touchmove', (e) => {
172
+ e.stopPropagation();
173
+ }, { passive: false });
174
+
175
+ // Additional scroll prevention for iOS and other edge cases
176
+ let touchStartY = 0;
177
+
178
+ // Track touch start position
179
+ tocContainer.addEventListener('touchstart', (e) => {
180
+ touchStartY = e.touches[0].clientY;
181
+ }, { passive: true });
182
+
183
+ // Prevent overscroll on TOC container
184
+ tocContainer.addEventListener('touchmove', (e) => {
185
+ const touchY = e.touches[0].clientY;
186
+ const scrollTop = tocContainer.scrollTop;
187
+ const scrollHeight = tocContainer.scrollHeight;
188
+ const height = tocContainer.clientHeight;
189
+ const isScrollingUp = touchY > touchStartY;
190
+ const isScrollingDown = touchY < touchStartY;
191
+
192
+ // Prevent scroll when at boundaries
193
+ if ((isScrollingUp && scrollTop <= 0) ||
194
+ (isScrollingDown && scrollTop + height >= scrollHeight)) {
195
+ e.preventDefault();
196
+ }
197
+ }, { passive: false });
198
+
199
+ // Prevent any scrolling on the backdrop
200
+ if (tocBackdrop) {
201
+ tocBackdrop.addEventListener('touchmove', (e) => {
202
+ e.preventDefault();
203
+ }, { passive: false });
204
+ }
205
+ }
206
+
207
+ // Add smooth scrolling and active state management
208
+ const tocLinks = tocList.querySelectorAll('.toc-link');
209
+ tocLinks.forEach(link => {
210
+ link.addEventListener('click', (e) => {
211
+ e.preventDefault();
212
+ const targetId = link.getAttribute('href').substring(1);
213
+ const target = document.getElementById(targetId);
214
+
215
+ if (target) {
216
+ // Close TOC first on mobile
217
+ if (window.innerWidth < tocBreakpoint) {
218
+ tocContainer.classList.remove('mobile-visible');
219
+ document.getElementById('toc-backdrop')?.classList.remove('visible');
220
+ document.body.classList.remove('toc-open');
221
+ document.body.style.top = '';
222
+ }
223
+
224
+ target.scrollIntoView({
225
+ behavior: 'smooth',
226
+ block: 'start'
227
+ });
228
+ tocLinks.forEach(l => l.classList.remove('active'));
229
+ link.classList.add('active');
230
+
231
+ }
232
+ });
233
+ });
234
+
235
+ // Add click handler for Contents title link
236
+ const tocTitleLink = document.getElementById('toc-title-link');
237
+ if (tocTitleLink) {
238
+ tocTitleLink.addEventListener('click', (e) => {
239
+ e.preventDefault();
240
+ scrollToTop();
241
+ });
242
+ }
243
+
244
+ // Scroll to top function
245
+ function scrollToTop() {
246
+ // Close TOC first on mobile
247
+ if (window.innerWidth < tocBreakpoint) {
248
+ tocContainer.classList.remove('mobile-visible');
249
+ document.getElementById('toc-backdrop')?.classList.remove('visible');
250
+ document.body.classList.remove('toc-open');
251
+ document.body.style.top = '';
252
+ }
253
+
254
+ // Scroll to top
255
+ window.scrollTo({
256
+ top: 0,
257
+ behavior: 'smooth'
258
+ });
259
+ }
260
+
261
+ // Helper function to check if TOC toggle should be visible
262
+ const updateTocToggleVisibility = () => {
263
+ if (tocToggle && tocLinks.length > 0) {
264
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
265
+ const activeLink = tocList.querySelector('.toc-link.active');
266
+ const firstTocLink = tocLinks[0];
267
+
268
+ // Only show toggle if:
269
+ // 1. We've scrolled down at least 100px from the top, AND
270
+ // 2. We're past the first section (activeLink exists and is not the first)
271
+ const hasScrolled = scrollTop > 100;
272
+ const isPastFirstSection = activeLink && activeLink !== firstTocLink;
273
+ const showToggle = hasScrolled && isPastFirstSection;
274
+
275
+ tocToggle.classList.toggle('show-toggle', showToggle);
276
+ }
277
+ };
278
+
279
+ // Intersection Observer for active state
280
+ const observerOptions = {
281
+ rootMargin: '-20% 0% -70% 0%',
282
+ threshold: 0
283
+ };
284
+ const observer = new IntersectionObserver((entries) => {
285
+ entries.forEach(entry => {
286
+ if (entry.isIntersecting) {
287
+ tocLinks.forEach(link => link.classList.remove('active'));
288
+ const activeLink = tocList.querySelector(`a[href="#${entry.target.id}"]`);
289
+ if (activeLink) {
290
+ activeLink.classList.add('active');
291
+ }
292
+ }
293
+ });
294
+
295
+ // Update toggle visibility after intersection changes
296
+ updateTocToggleVisibility();
297
+ }, observerOptions);
298
+
299
+ filteredHeadings.forEach(heading => observer.observe(heading));
300
+
301
+ // Update toggle visibility on scroll
302
+ let scrollTimeout;
303
+ window.addEventListener('scroll', () => {
304
+ // Throttle scroll events for performance
305
+ clearTimeout(scrollTimeout);
306
+ scrollTimeout = setTimeout(updateTocToggleVisibility, 16); // ~60fps
307
+ });
308
+
309
+ // Initial check
310
+ updateTocToggleVisibility();
311
+ }
312
+
313
+ // Initialize immediately, no setTimeout
314
+ document.addEventListener('DOMContentLoaded', () => {
315
+ initTOC();
316
+ if (typeof feather !== 'undefined') {
317
+ feather.replace();
318
+ }
319
+ });