kash-shell 0.3.28__py3-none-any.whl → 0.3.33__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/chat.py +1 -0
- kash/actions/core/markdownify_html.py +4 -5
- kash/actions/core/minify_html.py +4 -5
- kash/actions/core/readability.py +1 -4
- kash/actions/core/render_as_html.py +10 -7
- kash/actions/core/save_sidematter_meta.py +47 -0
- kash/actions/core/show_webpage.py +2 -0
- kash/actions/core/zip_sidematter.py +47 -0
- kash/commands/base/basic_file_commands.py +7 -4
- kash/commands/base/diff_commands.py +6 -4
- kash/commands/base/files_command.py +31 -30
- kash/commands/base/general_commands.py +3 -2
- kash/commands/base/logs_commands.py +6 -4
- kash/commands/base/reformat_command.py +3 -2
- kash/commands/base/search_command.py +4 -3
- kash/commands/base/show_command.py +9 -7
- kash/commands/help/assistant_commands.py +6 -4
- kash/commands/help/help_commands.py +7 -4
- kash/commands/workspace/selection_commands.py +18 -16
- kash/commands/workspace/workspace_commands.py +39 -26
- kash/config/logger.py +1 -1
- kash/config/setup.py +2 -27
- kash/config/text_styles.py +1 -1
- kash/docs/markdown/topics/a1_what_is_kash.md +26 -18
- kash/docs/markdown/topics/a2_installation.md +3 -2
- kash/exec/action_decorators.py +7 -5
- kash/exec/action_exec.py +104 -53
- kash/exec/fetch_url_items.py +40 -11
- kash/exec/llm_transforms.py +14 -5
- kash/exec/preconditions.py +2 -2
- kash/exec/resolve_args.py +4 -1
- kash/exec/runtime_settings.py +3 -0
- kash/file_storage/file_store.py +108 -114
- kash/file_storage/item_file_format.py +91 -26
- kash/file_storage/item_id_index.py +128 -0
- kash/help/help_types.py +1 -1
- kash/llm_utils/llms.py +6 -1
- kash/local_server/local_server_commands.py +2 -1
- kash/mcp/mcp_server_commands.py +3 -2
- kash/mcp/mcp_server_routes.py +42 -12
- kash/model/actions_model.py +44 -32
- kash/model/compound_actions_model.py +4 -3
- kash/model/exec_model.py +33 -3
- kash/model/items_model.py +150 -60
- kash/model/params_model.py +4 -4
- kash/shell/output/shell_output.py +1 -2
- kash/utils/api_utils/gather_limited.py +2 -0
- kash/utils/api_utils/multitask_gather.py +74 -0
- kash/utils/common/s3_utils.py +108 -0
- kash/utils/common/url.py +16 -4
- kash/utils/file_formats/chat_format.py +7 -4
- kash/utils/file_utils/file_ext.py +1 -0
- kash/utils/file_utils/file_formats.py +4 -2
- kash/utils/file_utils/file_formats_model.py +12 -0
- kash/utils/text_handling/doc_normalization.py +1 -1
- kash/utils/text_handling/markdown_footnotes.py +224 -0
- kash/utils/text_handling/markdown_utils.py +532 -41
- kash/utils/text_handling/markdownify_utils.py +2 -1
- kash/web_content/web_fetch.py +2 -1
- kash/web_gen/templates/components/tooltip_scripts.js.jinja +186 -1
- kash/web_gen/templates/components/youtube_popover_scripts.js.jinja +223 -0
- kash/web_gen/templates/components/youtube_popover_styles.css.jinja +150 -0
- kash/web_gen/templates/content_styles.css.jinja +53 -1
- kash/web_gen/templates/youtube_webpage.html.jinja +47 -0
- kash/web_gen/webpage_render.py +103 -0
- kash/workspaces/workspaces.py +0 -5
- kash/xonsh_custom/custom_shell.py +4 -3
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.33.dist-info}/METADATA +35 -26
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.33.dist-info}/RECORD +72 -64
- kash/llm_utils/llm_features.py +0 -72
- kash/web_gen/simple_webpage.py +0 -55
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.33.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.33.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.28.dist-info → kash_shell-0.3.33.dist-info}/licenses/LICENSE +0 -0
kash/web_content/web_fetch.py
CHANGED
|
@@ -293,7 +293,8 @@ def download_url(
|
|
|
293
293
|
|
|
294
294
|
s3 = boto3.resource("s3")
|
|
295
295
|
s3_path = parsed_url.path.lstrip("/")
|
|
296
|
-
|
|
296
|
+
with atomic_output_file(target_filename, make_parents=True) as temp_filename:
|
|
297
|
+
s3.Bucket(parsed_url.netloc).download_file(s3_path, temp_filename)
|
|
297
298
|
return None
|
|
298
299
|
|
|
299
300
|
req_headers = _get_req_headers(mode, headers)
|
|
@@ -41,6 +41,8 @@ const TOOLTIP_CONFIG = {
|
|
|
41
41
|
content: {
|
|
42
42
|
maxFootnoteLength: 400, // Maximum characters for footnote tooltips
|
|
43
43
|
minFootnoteLength: 5, // Minimum characters to show tooltip
|
|
44
|
+
maxInternalLinkLength: 1000, // Maximum characters for internal link tooltips
|
|
45
|
+
minInternalLinkLength: 10, // Minimum characters to show tooltip
|
|
44
46
|
}
|
|
45
47
|
};
|
|
46
48
|
|
|
@@ -859,6 +861,187 @@ function truncateFootnoteContent(html, text) {
|
|
|
859
861
|
return tempDiv.innerHTML;
|
|
860
862
|
}
|
|
861
863
|
|
|
864
|
+
/* -----------------------------------------------------------------------------
|
|
865
|
+
Automatic internal link detection and tooltip creation
|
|
866
|
+
----------------------------------------------------------------------------- */
|
|
867
|
+
|
|
868
|
+
// Initialize internal link tooltips
|
|
869
|
+
function initInternalLinkTooltips() {
|
|
870
|
+
console.debug('Initializing internal link tooltips...');
|
|
871
|
+
|
|
872
|
+
// Find all internal links (href starting with #, but not footnotes)
|
|
873
|
+
// Note: :not() cannot contain complex selectors like descendant selectors
|
|
874
|
+
const allInternalLinks = document.querySelectorAll('a[href^="#"]:not(.footnote)');
|
|
875
|
+
const internalLinks = Array.from(allInternalLinks).filter(link => {
|
|
876
|
+
// Skip if it's inside a footnote-ref
|
|
877
|
+
if (link.closest('.footnote-ref')) return false;
|
|
878
|
+
|
|
879
|
+
const href = link.getAttribute('href');
|
|
880
|
+
// Skip empty or footnote links
|
|
881
|
+
return href && href !== '#' && !href.startsWith('#fn-');
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
let tooltipsAdded = 0;
|
|
885
|
+
|
|
886
|
+
internalLinks.forEach(link => {
|
|
887
|
+
try {
|
|
888
|
+
const href = link.getAttribute('href');
|
|
889
|
+
if (!href || href === '#' || href.startsWith('#fn-')) return; // Skip empty or footnote links
|
|
890
|
+
|
|
891
|
+
const targetId = href.substring(1);
|
|
892
|
+
const targetElement = document.getElementById(targetId);
|
|
893
|
+
if (!targetElement) {
|
|
894
|
+
console.debug(`Target element not found for ID: ${targetId}`);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Extract meaningful content based on element type
|
|
899
|
+
let tooltipContent = '';
|
|
900
|
+
let tooltipText = '';
|
|
901
|
+
|
|
902
|
+
// Check if it's a heading
|
|
903
|
+
if (targetElement.matches('h1, h2, h3, h4, h5, h6')) {
|
|
904
|
+
const headingLevel = targetElement.tagName.substring(1);
|
|
905
|
+
const headingText = TooltipUtils.extractTextContent(targetElement);
|
|
906
|
+
tooltipText = headingText;
|
|
907
|
+
|
|
908
|
+
// Format based on heading level
|
|
909
|
+
if (headingLevel === '1' || headingLevel === '2') {
|
|
910
|
+
tooltipContent = `<strong>Section:</strong> ${TooltipUtils.sanitizeText(headingText)}`;
|
|
911
|
+
} else {
|
|
912
|
+
tooltipContent = `<strong>Subsection:</strong> ${TooltipUtils.sanitizeText(headingText)}`;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Try to get first paragraph after heading for context
|
|
916
|
+
let nextElement = targetElement.nextElementSibling;
|
|
917
|
+
while (nextElement && !nextElement.matches('p') && !nextElement.matches('h1, h2, h3, h4, h5, h6')) {
|
|
918
|
+
nextElement = nextElement.nextElementSibling;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (nextElement && nextElement.matches('p')) {
|
|
922
|
+
const paragraphText = TooltipUtils.extractTextContent(nextElement);
|
|
923
|
+
if (paragraphText && paragraphText.length > 0) {
|
|
924
|
+
const truncatedPara = truncateInternalLinkContent(paragraphText, 150);
|
|
925
|
+
tooltipContent += `<br><br>${TooltipUtils.sanitizeText(truncatedPara)}`;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
// Check if it's a figure, table, or other special content
|
|
930
|
+
else if (targetElement.matches('figure, .figure')) {
|
|
931
|
+
const caption = targetElement.querySelector('figcaption, .caption');
|
|
932
|
+
if (caption) {
|
|
933
|
+
tooltipText = TooltipUtils.extractTextContent(caption);
|
|
934
|
+
tooltipContent = `<strong>Figure:</strong> ${TooltipUtils.sanitizeText(tooltipText)}`;
|
|
935
|
+
} else {
|
|
936
|
+
tooltipText = 'Figure';
|
|
937
|
+
tooltipContent = '<strong>Figure</strong>';
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
else if (targetElement.matches('table, .table-container')) {
|
|
941
|
+
const caption = targetElement.querySelector('caption');
|
|
942
|
+
const firstHeader = targetElement.querySelector('th');
|
|
943
|
+
if (caption) {
|
|
944
|
+
tooltipText = TooltipUtils.extractTextContent(caption);
|
|
945
|
+
tooltipContent = `<strong>Table:</strong> ${TooltipUtils.sanitizeText(tooltipText)}`;
|
|
946
|
+
} else if (firstHeader) {
|
|
947
|
+
tooltipText = TooltipUtils.extractTextContent(firstHeader);
|
|
948
|
+
tooltipContent = `<strong>Table:</strong> ${TooltipUtils.sanitizeText(tooltipText)}...`;
|
|
949
|
+
} else {
|
|
950
|
+
tooltipText = 'Table';
|
|
951
|
+
tooltipContent = '<strong>Table</strong>';
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
// Check if it's a code block
|
|
955
|
+
else if (targetElement.matches('pre, .code-block-wrapper')) {
|
|
956
|
+
const codeElement = targetElement.querySelector('code') || targetElement;
|
|
957
|
+
const codeText = TooltipUtils.extractTextContent(codeElement);
|
|
958
|
+
const firstLine = codeText.split('\n')[0];
|
|
959
|
+
tooltipText = firstLine;
|
|
960
|
+
tooltipContent = `<strong>Code:</strong> <code>${TooltipUtils.sanitizeText(truncateInternalLinkContent(firstLine, 100))}</code>`;
|
|
961
|
+
}
|
|
962
|
+
// Check if it's a details/summary element
|
|
963
|
+
else if (targetElement.matches('details')) {
|
|
964
|
+
const summary = targetElement.querySelector('summary');
|
|
965
|
+
if (summary) {
|
|
966
|
+
tooltipText = TooltipUtils.extractTextContent(summary);
|
|
967
|
+
tooltipContent = `<strong>Details:</strong> ${TooltipUtils.sanitizeText(tooltipText)}`;
|
|
968
|
+
} else {
|
|
969
|
+
return; // Skip if no summary
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// Default: extract text content from the element
|
|
973
|
+
else {
|
|
974
|
+
tooltipText = TooltipUtils.extractTextContent(targetElement);
|
|
975
|
+
|
|
976
|
+
// Check if the element has a meaningful tag name for context
|
|
977
|
+
const tagName = targetElement.tagName.toLowerCase();
|
|
978
|
+
if (tagName === 'section' || tagName === 'article') {
|
|
979
|
+
// Try to find a heading within it
|
|
980
|
+
const heading = targetElement.querySelector('h1, h2, h3, h4, h5, h6');
|
|
981
|
+
if (heading) {
|
|
982
|
+
const headingText = TooltipUtils.extractTextContent(heading);
|
|
983
|
+
tooltipContent = `<strong>Section:</strong> ${TooltipUtils.sanitizeText(headingText)}`;
|
|
984
|
+
// Get first paragraph if available
|
|
985
|
+
const firstPara = targetElement.querySelector('p');
|
|
986
|
+
if (firstPara) {
|
|
987
|
+
const paraText = TooltipUtils.extractTextContent(firstPara);
|
|
988
|
+
const truncatedPara = truncateInternalLinkContent(paraText, 150);
|
|
989
|
+
tooltipContent += `<br><br>${TooltipUtils.sanitizeText(truncatedPara)}`;
|
|
990
|
+
}
|
|
991
|
+
} else {
|
|
992
|
+
tooltipContent = TooltipUtils.sanitizeText(truncateInternalLinkContent(tooltipText, TOOLTIP_CONFIG.content.maxInternalLinkLength));
|
|
993
|
+
}
|
|
994
|
+
} else {
|
|
995
|
+
// Just show the content
|
|
996
|
+
tooltipContent = TooltipUtils.sanitizeText(truncateInternalLinkContent(tooltipText, TOOLTIP_CONFIG.content.maxInternalLinkLength));
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Skip if content is too short or empty
|
|
1001
|
+
if (!tooltipText || tooltipText.length < TOOLTIP_CONFIG.content.minInternalLinkLength) {
|
|
1002
|
+
console.debug(`Content too short or empty for internal link to: ${targetId}`);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Add the tooltip
|
|
1007
|
+
TooltipManager.addTooltip(link, tooltipContent, 'auto', {
|
|
1008
|
+
isInternalLink: true,
|
|
1009
|
+
targetId: targetId
|
|
1010
|
+
});
|
|
1011
|
+
tooltipsAdded++;
|
|
1012
|
+
|
|
1013
|
+
console.debug(`Added tooltip for internal link to ${targetId}: "${tooltipText.substring(0, 50)}..."`);
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
console.error('Error processing internal link:', link, error);
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
console.debug(`Internal link tooltips initialized: ${tooltipsAdded} tooltips added`);
|
|
1020
|
+
return tooltipsAdded;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Truncate internal link content if too long
|
|
1024
|
+
function truncateInternalLinkContent(text, maxLength = TOOLTIP_CONFIG.content.maxInternalLinkLength) {
|
|
1025
|
+
if (!text) return '';
|
|
1026
|
+
|
|
1027
|
+
const trimmedText = text.trim();
|
|
1028
|
+
if (trimmedText.length <= maxLength) {
|
|
1029
|
+
return trimmedText;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Try to truncate at a word boundary
|
|
1033
|
+
const truncated = trimmedText.substring(0, maxLength);
|
|
1034
|
+
const lastSpace = truncated.lastIndexOf(' ');
|
|
1035
|
+
|
|
1036
|
+
if (lastSpace > maxLength * 0.8) {
|
|
1037
|
+
// If we have a reasonable word boundary, use it
|
|
1038
|
+
return truncated.substring(0, lastSpace) + '…';
|
|
1039
|
+
} else {
|
|
1040
|
+
// Otherwise just truncate at the max length
|
|
1041
|
+
return truncated + '…';
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
862
1045
|
// Initialize all tooltips
|
|
863
1046
|
function initTooltips() {
|
|
864
1047
|
console.debug('Starting tooltip initialization...');
|
|
@@ -868,7 +1051,9 @@ function initTooltips() {
|
|
|
868
1051
|
MobileInteractionManager.init();
|
|
869
1052
|
|
|
870
1053
|
const footnoteCount = initFootnoteTooltips();
|
|
871
|
-
|
|
1054
|
+
const internalLinkCount = initInternalLinkTooltips();
|
|
1055
|
+
|
|
1056
|
+
console.debug(`Tooltip initialization complete. Footnote tooltips: ${footnoteCount}, Internal link tooltips: ${internalLinkCount}`);
|
|
872
1057
|
} catch (error) {
|
|
873
1058
|
console.error('Error during tooltip initialization:', error);
|
|
874
1059
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
function isYouTubeLink(href) {
|
|
3
|
+
return /(^https?:\/\/)?(www\.)?(youtube\.com\/watch|youtu\.be\/|youtube\.com\/shorts\/|youtube\.com\/embed\/)/.test(href);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function parseTimeToSeconds(text) {
|
|
7
|
+
if (!text) return 0;
|
|
8
|
+
// Support common formats like "170.56s" or "170.56" (seconds, possibly decimal)
|
|
9
|
+
const trimmed = String(text).trim();
|
|
10
|
+
const cleaned = trimmed.endsWith('s') ? trimmed.slice(0, -1) : trimmed;
|
|
11
|
+
const seconds = parseFloat(cleaned);
|
|
12
|
+
if (!isFinite(seconds) || seconds < 0) return 0;
|
|
13
|
+
// YouTube embed "start" expects integer seconds
|
|
14
|
+
return Math.floor(seconds);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseStartSeconds(url) {
|
|
18
|
+
try {
|
|
19
|
+
const u = new URL(url, window.location.origin);
|
|
20
|
+
if (u.searchParams.has("t")) {
|
|
21
|
+
return parseTimeToSeconds(u.searchParams.get("t") || "0");
|
|
22
|
+
}
|
|
23
|
+
if (u.searchParams.has("start")) {
|
|
24
|
+
return parseTimeToSeconds(u.searchParams.get("start") || "0");
|
|
25
|
+
}
|
|
26
|
+
// #t=...
|
|
27
|
+
if (u.hash && u.hash.startsWith("#t=")) {
|
|
28
|
+
return parseTimeToSeconds(u.hash.slice(3));
|
|
29
|
+
}
|
|
30
|
+
return 0;
|
|
31
|
+
} catch {
|
|
32
|
+
// Fallback rough parse
|
|
33
|
+
const m = url.match(/[?#&](?:start|t)=([^&#]+)/);
|
|
34
|
+
return m ? parseTimeToSeconds(decodeURIComponent(m[1])) : 0;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function extractVideoId(url) {
|
|
39
|
+
try {
|
|
40
|
+
const u = new URL(url, window.location.origin);
|
|
41
|
+
const host = u.hostname.replace(/^www\./, "");
|
|
42
|
+
if (host === "youtu.be") {
|
|
43
|
+
return u.pathname.split("/").filter(Boolean)[0] || null;
|
|
44
|
+
}
|
|
45
|
+
if (host === "youtube.com" || host === "m.youtube.com" || host === "youtube-nocookie.com") {
|
|
46
|
+
if (u.pathname.startsWith("/watch")) {
|
|
47
|
+
return u.searchParams.get("v");
|
|
48
|
+
}
|
|
49
|
+
if (u.pathname.startsWith("/embed/") || u.pathname.startsWith("/shorts/")) {
|
|
50
|
+
return u.pathname.split("/")[2] || null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
} catch {
|
|
55
|
+
// Fallback regexes
|
|
56
|
+
const rx1 = /youtu\.be\/([^?&#\/]+)/;
|
|
57
|
+
const rx2 = /v=([^?&#\/]+)/;
|
|
58
|
+
const rx3 = /\/embed\/([^?&#\/]+)/;
|
|
59
|
+
const rx4 = /youtube\.com\/shorts\/([^?&#\/]+)/;
|
|
60
|
+
return (url.match(rx1)?.[1]) || (url.match(rx2)?.[1]) || (url.match(rx3)?.[1]) || (url.match(rx4)?.[1]) || null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildEmbedUrl(videoId, startSeconds) {
|
|
65
|
+
const params = new URLSearchParams({
|
|
66
|
+
autoplay: "1",
|
|
67
|
+
start: String(startSeconds || 0),
|
|
68
|
+
rel: "0",
|
|
69
|
+
modestbranding: "1",
|
|
70
|
+
enablejsapi: "1"
|
|
71
|
+
});
|
|
72
|
+
return `https://www.youtube.com/embed/${encodeURIComponent(videoId)}?${params.toString()}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function openPopover({ href, title }) {
|
|
76
|
+
const pop = document.getElementById("yt-popover");
|
|
77
|
+
if (!pop) return;
|
|
78
|
+
|
|
79
|
+
const id = extractVideoId(href);
|
|
80
|
+
if (!id) return;
|
|
81
|
+
|
|
82
|
+
const start = parseStartSeconds(href);
|
|
83
|
+
const iframe = pop.querySelector("#yt-iframe");
|
|
84
|
+
const titleEl = pop.querySelector("#yt-title");
|
|
85
|
+
const backdrop = document.getElementById("yt-backdrop");
|
|
86
|
+
|
|
87
|
+
// Set title and src
|
|
88
|
+
titleEl.textContent = title || "YouTube";
|
|
89
|
+
iframe.src = buildEmbedUrl(id, start);
|
|
90
|
+
|
|
91
|
+
pop.classList.add("open");
|
|
92
|
+
pop.setAttribute("aria-hidden", "false");
|
|
93
|
+
|
|
94
|
+
// Show mobile backdrop and lock body like TOC does
|
|
95
|
+
backdrop?.classList.add("visible");
|
|
96
|
+
document.body.classList.add("yt-open");
|
|
97
|
+
|
|
98
|
+
// Gracefully hide TOC if present, and remember to restore it later
|
|
99
|
+
tryHideTOC();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function closePopover() {
|
|
103
|
+
const pop = document.getElementById("yt-popover");
|
|
104
|
+
if (!pop) return;
|
|
105
|
+
const iframe = pop.querySelector("#yt-iframe");
|
|
106
|
+
const backdrop = document.getElementById("yt-backdrop");
|
|
107
|
+
// Stop playback by clearing src
|
|
108
|
+
iframe.src = "";
|
|
109
|
+
pop.classList.remove("open", "maximized");
|
|
110
|
+
pop.setAttribute("aria-hidden", "true");
|
|
111
|
+
backdrop?.classList.remove("visible");
|
|
112
|
+
document.body.classList.remove("yt-open");
|
|
113
|
+
|
|
114
|
+
// Restore TOC state if we hid it
|
|
115
|
+
tryRestoreTOC();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function toggleMaximize() {
|
|
119
|
+
const pop = document.getElementById("yt-popover");
|
|
120
|
+
if (!pop) return;
|
|
121
|
+
const btn = pop.querySelector(".yt-maximize");
|
|
122
|
+
const isMax = pop.classList.toggle("maximized");
|
|
123
|
+
// Swap icon
|
|
124
|
+
if (typeof feather !== "undefined" && btn) {
|
|
125
|
+
btn.innerHTML = isMax ? feather.icons.minimize.toSvg() : feather.icons.maximize.toSvg();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function attachHandlers() {
|
|
130
|
+
// Wire up static controls
|
|
131
|
+
const pop = document.getElementById("yt-popover");
|
|
132
|
+
if (!pop) return;
|
|
133
|
+
|
|
134
|
+
const closeBtn = pop.querySelector(".yt-close");
|
|
135
|
+
const maxBtn = pop.querySelector(".yt-maximize");
|
|
136
|
+
const backdrop = document.getElementById("yt-backdrop");
|
|
137
|
+
|
|
138
|
+
closeBtn?.addEventListener("click", (e) => {
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
closePopover();
|
|
141
|
+
});
|
|
142
|
+
maxBtn?.addEventListener("click", (e) => {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
toggleMaximize();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ESC closes
|
|
148
|
+
document.addEventListener("keydown", (e) => {
|
|
149
|
+
if (e.key === "Escape") {
|
|
150
|
+
closePopover();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Click on backdrop closes (mobile)
|
|
155
|
+
backdrop?.addEventListener("click", () => closePopover());
|
|
156
|
+
|
|
157
|
+
// Intercept YouTube links inside main content
|
|
158
|
+
const scope = document.querySelector(".long-text") || document;
|
|
159
|
+
scope.querySelectorAll('a[href]').forEach(a => {
|
|
160
|
+
const href = a.getAttribute("href") || "";
|
|
161
|
+
if (!isYouTubeLink(href)) return;
|
|
162
|
+
|
|
163
|
+
a.addEventListener("click", (ev) => {
|
|
164
|
+
// Respect new-tab or modifier-click intentions
|
|
165
|
+
if (ev.defaultPrevented) return;
|
|
166
|
+
if (ev.button !== 0) return; // only left click
|
|
167
|
+
if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
|
|
168
|
+
|
|
169
|
+
ev.preventDefault();
|
|
170
|
+
ev.stopPropagation();
|
|
171
|
+
|
|
172
|
+
openPopover({ href, title: a.textContent?.trim() || "YouTube" });
|
|
173
|
+
|
|
174
|
+
// Update icons if necessary
|
|
175
|
+
if (typeof feather !== "undefined") {
|
|
176
|
+
feather.replace();
|
|
177
|
+
}
|
|
178
|
+
}, { capture: true });
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---- TOC coexistence helpers ----
|
|
183
|
+
function tryHideTOC() {
|
|
184
|
+
const tocContainer = document.getElementById('toc-container');
|
|
185
|
+
const tocBackdrop = document.getElementById('toc-backdrop');
|
|
186
|
+
if (!tocContainer) return;
|
|
187
|
+
|
|
188
|
+
// Desktop layout is auto-hidden via CSS when body has .yt-open
|
|
189
|
+
// For mobile layout, if TOC is currently open, close it and remember to restore
|
|
190
|
+
const wasOpenMobile = tocContainer.classList.contains('mobile-visible');
|
|
191
|
+
const bodyHadTocOpen = document.body.classList.contains('toc-open');
|
|
192
|
+
|
|
193
|
+
if (wasOpenMobile) {
|
|
194
|
+
document.body.dataset.tocWasOpen = '1';
|
|
195
|
+
tocContainer.classList.remove('mobile-visible');
|
|
196
|
+
tocBackdrop?.classList.remove('visible');
|
|
197
|
+
}
|
|
198
|
+
if (bodyHadTocOpen) {
|
|
199
|
+
document.body.dataset.tocHadBodyOpen = '1';
|
|
200
|
+
document.body.classList.remove('toc-open');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function tryRestoreTOC() {
|
|
205
|
+
const tocContainer = document.getElementById('toc-container');
|
|
206
|
+
const tocBackdrop = document.getElementById('toc-backdrop');
|
|
207
|
+
if (!tocContainer) return;
|
|
208
|
+
|
|
209
|
+
if (document.body.dataset.tocWasOpen === '1') {
|
|
210
|
+
tocContainer.classList.add('mobile-visible');
|
|
211
|
+
tocBackdrop?.classList.add('visible');
|
|
212
|
+
delete document.body.dataset.tocWasOpen;
|
|
213
|
+
}
|
|
214
|
+
if (document.body.dataset.tocHadBodyOpen === '1') {
|
|
215
|
+
document.body.classList.add('toc-open');
|
|
216
|
+
delete document.body.dataset.tocHadBodyOpen;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
document.addEventListener("DOMContentLoaded", attachHandlers);
|
|
221
|
+
})();
|
|
222
|
+
|
|
223
|
+
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/* YouTube popover styles aligned to the TOC layout */
|
|
2
|
+
.yt-popover {
|
|
3
|
+
position: fixed;
|
|
4
|
+
display: none; /* hidden by default; toggled via .open */
|
|
5
|
+
background: var(--color-bg);
|
|
6
|
+
color: var(--color-text);
|
|
7
|
+
border: 1px solid var(--color-border-hint); /* match TOC border */
|
|
8
|
+
border-radius: 0; /* match TOC radius */
|
|
9
|
+
box-shadow: none; /* match TOC (no shadow) */
|
|
10
|
+
z-index: 200;
|
|
11
|
+
overflow: hidden;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.yt-popover.open { display: flex; flex-direction: column; }
|
|
15
|
+
|
|
16
|
+
.yt-popover-header {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: space-between;
|
|
20
|
+
gap: 0.5rem;
|
|
21
|
+
padding: 0.5rem 0.5rem 0.5rem 0.75rem;
|
|
22
|
+
background: var(--color-bg-alt);
|
|
23
|
+
border-bottom: 1px solid var(--color-hint-gentle);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.yt-title {
|
|
27
|
+
font-family: 'Source Sans 3 Variable', system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
28
|
+
font-size: 0.95rem;
|
|
29
|
+
line-height: 1.2;
|
|
30
|
+
color: var(--color-hint-strong);
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
text-overflow: ellipsis;
|
|
33
|
+
white-space: nowrap;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.yt-controls { display: flex; gap: 0.35rem; }
|
|
37
|
+
|
|
38
|
+
.yt-player-wrap {
|
|
39
|
+
position: relative;
|
|
40
|
+
flex: 1 1 auto;
|
|
41
|
+
background: #000;
|
|
42
|
+
min-height: 200px; /* ensure visible player area on small screens */
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.yt-iframe {
|
|
46
|
+
position: absolute;
|
|
47
|
+
inset: 0;
|
|
48
|
+
width: 100%;
|
|
49
|
+
height: 100%;
|
|
50
|
+
border: 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Maximize fills available viewport width while keeping margins */
|
|
54
|
+
.yt-popover.maximized {
|
|
55
|
+
left: 1rem !important;
|
|
56
|
+
right: 1rem !important;
|
|
57
|
+
top: 1rem !important;
|
|
58
|
+
bottom: 1rem !important;
|
|
59
|
+
width: auto !important;
|
|
60
|
+
border-radius: 0.5rem; /* maximize uses a gentle radius to indicate modal-like state */
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Use default .button hover and pointer behavior from base styles */
|
|
64
|
+
|
|
65
|
+
/* Desktop layout: overlay on the left, same width as TOC */
|
|
66
|
+
@media (min-width: {{ toc_breakpoint | default(1200) }}px) {
|
|
67
|
+
.yt-popover {
|
|
68
|
+
top: 2rem; /* align with TOC sticky top */
|
|
69
|
+
left: 2rem; /* align with TOC left margin */
|
|
70
|
+
width: calc(var(--toc-width) * 1.4);
|
|
71
|
+
min-height: var(--toc-width);
|
|
72
|
+
overflow: hidden; /* avoid internal scrollbars; player fills */
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* When YouTube popover is open, hide the TOC on desktop */
|
|
76
|
+
body.yt-open .toc-container {
|
|
77
|
+
display: none !important;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Mobile layout: behave like TOC popover with dimmed backdrop */
|
|
82
|
+
@media (max-width: {{ toc_breakpoint | default(1200) - 1 }}px) {
|
|
83
|
+
.yt-backdrop {
|
|
84
|
+
position: fixed;
|
|
85
|
+
top: 0;
|
|
86
|
+
left: 0;
|
|
87
|
+
width: 100vw;
|
|
88
|
+
height: 100vh;
|
|
89
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
90
|
+
z-index: 199;
|
|
91
|
+
opacity: 0;
|
|
92
|
+
visibility: hidden;
|
|
93
|
+
pointer-events: none;
|
|
94
|
+
transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.yt-backdrop.visible {
|
|
98
|
+
opacity: 1;
|
|
99
|
+
visibility: visible;
|
|
100
|
+
pointer-events: auto;
|
|
101
|
+
touch-action: none;
|
|
102
|
+
-webkit-overflow-scrolling: auto;
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.yt-popover {
|
|
107
|
+
top: 4rem;
|
|
108
|
+
left: 1rem;
|
|
109
|
+
width: calc(100vw - 2rem);
|
|
110
|
+
height: calc(100vh - 5rem); /* definite height so player can flex-fill */
|
|
111
|
+
color: var(--color-text);
|
|
112
|
+
background: var(--color-bg-alt-solid); /* match TOC mobile background */
|
|
113
|
+
border: 1px solid var(--color-border-hint);
|
|
114
|
+
z-index: 200;
|
|
115
|
+
overflow: hidden; /* avoid scroll; header+player fit */
|
|
116
|
+
-webkit-overflow-scrolling: touch;
|
|
117
|
+
overscroll-behavior: contain;
|
|
118
|
+
touch-action: pan-y;
|
|
119
|
+
opacity: 0;
|
|
120
|
+
transform: translateY(-0.5rem);
|
|
121
|
+
visibility: hidden;
|
|
122
|
+
pointer-events: none;
|
|
123
|
+
transition: opacity 0.3s ease-in-out,
|
|
124
|
+
transform 0.3s ease-in-out,
|
|
125
|
+
visibility 0.3s ease-in-out,
|
|
126
|
+
pointer-events 0.3s ease-in-out,
|
|
127
|
+
background-color 0.4s ease-in-out,
|
|
128
|
+
border-color 0.4s ease-in-out,
|
|
129
|
+
box-shadow 0.4s ease-in-out;
|
|
130
|
+
padding: 1rem 0.7rem; /* match TOC mobile padding */
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.yt-popover.open {
|
|
134
|
+
opacity: 1;
|
|
135
|
+
transform: translateY(0);
|
|
136
|
+
visibility: visible;
|
|
137
|
+
pointer-events: auto;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Prevent body scroll when YouTube popover is open */
|
|
141
|
+
body.yt-open {
|
|
142
|
+
overflow: hidden;
|
|
143
|
+
position: fixed;
|
|
144
|
+
width: 100%;
|
|
145
|
+
touch-action: none;
|
|
146
|
+
-webkit-overflow-scrolling: auto;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
@@ -30,10 +30,19 @@
|
|
|
30
30
|
color: var(--color-primary);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
{# Useful for debugging chunking #}
|
|
34
|
+
{# .chunk {
|
|
34
35
|
padding-top: 0.5rem;
|
|
35
36
|
padding-bottom: 0.5rem;
|
|
36
37
|
border-top: 1px dashed var(--color-hint);
|
|
38
|
+
} #}
|
|
39
|
+
|
|
40
|
+
.debug {
|
|
41
|
+
color: var(--color-hint) !important;
|
|
42
|
+
font-size: var(--font-size-tiny) !important;
|
|
43
|
+
font-family: var(--font-sans) !important;
|
|
44
|
+
font-feature-settings: var(--font-features-sans) !important;
|
|
45
|
+
font-weight: 400 !important;
|
|
37
46
|
}
|
|
38
47
|
|
|
39
48
|
.description {
|
|
@@ -44,6 +53,49 @@
|
|
|
44
53
|
margin: 2rem 0;
|
|
45
54
|
}
|
|
46
55
|
|
|
56
|
+
.key-claims {
|
|
57
|
+
font-family: var(--font-sans);
|
|
58
|
+
font-feature-settings: var(--font-features-sans);
|
|
59
|
+
font-size: var(--font-size-small);
|
|
60
|
+
margin: 1rem 0;
|
|
61
|
+
padding: 1rem;
|
|
62
|
+
border: 1px solid var(--color-hint-gentle);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.claim {
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
position: relative;
|
|
68
|
+
margin-left: 1.8rem;
|
|
69
|
+
margin-top: 0.7rem;
|
|
70
|
+
margin-bottom: 0.7rem;
|
|
71
|
+
font-weight: var(--font-weight-sans-bold);
|
|
72
|
+
font-family: var(--font-sans);
|
|
73
|
+
font-feature-settings: var(--font-features-sans);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.claim::before {
|
|
77
|
+
content: "▪︎";
|
|
78
|
+
position: absolute;
|
|
79
|
+
left: -.85rem;
|
|
80
|
+
top: .25rem;
|
|
81
|
+
font-size: 0.625rem;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.key-claims::before {
|
|
85
|
+
content: "Key Claims";
|
|
86
|
+
display: block;
|
|
87
|
+
text-align: center;
|
|
88
|
+
font-family: var(--font-sans);
|
|
89
|
+
font-feature-settings: var(--font-features-sans);
|
|
90
|
+
font-weight: 500;
|
|
91
|
+
font-size: calc(1.2rem * var(--caps-heading-size-multiplier));
|
|
92
|
+
line-height: var(--caps-heading-line-height);
|
|
93
|
+
text-transform: var(--caps-transform);
|
|
94
|
+
font-variant-caps: var(--caps-caps-variant);
|
|
95
|
+
letter-spacing: var(--caps-spacing);
|
|
96
|
+
margin-bottom: 0.5rem;
|
|
97
|
+
}
|
|
98
|
+
|
|
47
99
|
.summary {
|
|
48
100
|
font-family: var(--font-sans);
|
|
49
101
|
font-feature-settings: var(--font-features-sans);
|