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.
- kash/actions/core/minify_html.py +41 -0
- kash/commands/base/show_command.py +11 -1
- kash/config/colors.py +6 -2
- kash/docs/markdown/topics/a1_what_is_kash.md +52 -23
- kash/docs/markdown/topics/a2_installation.md +17 -30
- kash/docs/markdown/topics/a3_getting_started.md +5 -19
- kash/exec/action_exec.py +1 -1
- kash/exec/fetch_url_metadata.py +9 -0
- kash/exec/precondition_registry.py +3 -3
- kash/file_storage/file_store.py +18 -1
- kash/llm_utils/llm_features.py +5 -1
- kash/llm_utils/llms.py +18 -8
- kash/media_base/media_cache.py +48 -24
- kash/media_base/media_services.py +63 -14
- kash/media_base/services/local_file_media.py +9 -1
- kash/model/items_model.py +4 -5
- kash/model/media_model.py +9 -1
- kash/model/params_model.py +9 -3
- kash/utils/common/function_inspect.py +97 -1
- kash/utils/common/testing.py +58 -0
- kash/utils/common/url_slice.py +329 -0
- kash/utils/file_utils/file_formats.py +1 -1
- kash/utils/text_handling/markdown_utils.py +424 -16
- kash/web_gen/templates/base_styles.css.jinja +137 -15
- kash/web_gen/templates/base_webpage.html.jinja +13 -17
- kash/web_gen/templates/components/toc_scripts.js.jinja +319 -0
- kash/web_gen/templates/components/toc_styles.css.jinja +284 -0
- kash/web_gen/templates/components/tooltip_scripts.js.jinja +730 -0
- kash/web_gen/templates/components/tooltip_styles.css.jinja +482 -0
- kash/web_gen/templates/content_styles.css.jinja +13 -8
- kash/web_gen/templates/simple_webpage.html.jinja +15 -481
- kash/workspaces/workspaces.py +10 -1
- {kash_shell-0.3.17.dist-info → kash_shell-0.3.18.dist-info}/METADATA +75 -72
- {kash_shell-0.3.17.dist-info → kash_shell-0.3.18.dist-info}/RECORD +37 -30
- {kash_shell-0.3.17.dist-info → kash_shell-0.3.18.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.17.dist-info → kash_shell-0.3.18.dist-info}/entry_points.txt +0 -0
- {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-
|
|
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.
|
|
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.
|
|
134
|
-
margin-top: 1.
|
|
135
|
-
margin-bottom: 0.
|
|
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:
|
|
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.
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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.
|
|
79
|
-
height: 2.
|
|
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:
|
|
95
|
-
height:
|
|
96
|
-
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
|
+
});
|