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.
Files changed (74) hide show
  1. kash/actions/core/chat.py +1 -0
  2. kash/actions/core/markdownify_html.py +4 -5
  3. kash/actions/core/minify_html.py +4 -5
  4. kash/actions/core/readability.py +1 -4
  5. kash/actions/core/render_as_html.py +10 -7
  6. kash/actions/core/save_sidematter_meta.py +47 -0
  7. kash/actions/core/show_webpage.py +2 -0
  8. kash/actions/core/zip_sidematter.py +47 -0
  9. kash/commands/base/basic_file_commands.py +7 -4
  10. kash/commands/base/diff_commands.py +6 -4
  11. kash/commands/base/files_command.py +31 -30
  12. kash/commands/base/general_commands.py +3 -2
  13. kash/commands/base/logs_commands.py +6 -4
  14. kash/commands/base/reformat_command.py +3 -2
  15. kash/commands/base/search_command.py +4 -3
  16. kash/commands/base/show_command.py +9 -7
  17. kash/commands/help/assistant_commands.py +6 -4
  18. kash/commands/help/help_commands.py +7 -4
  19. kash/commands/workspace/selection_commands.py +18 -16
  20. kash/commands/workspace/workspace_commands.py +39 -26
  21. kash/config/logger.py +1 -1
  22. kash/config/setup.py +2 -27
  23. kash/config/text_styles.py +1 -1
  24. kash/docs/markdown/topics/a1_what_is_kash.md +26 -18
  25. kash/docs/markdown/topics/a2_installation.md +3 -2
  26. kash/exec/action_decorators.py +7 -5
  27. kash/exec/action_exec.py +104 -53
  28. kash/exec/fetch_url_items.py +40 -11
  29. kash/exec/llm_transforms.py +14 -5
  30. kash/exec/preconditions.py +2 -2
  31. kash/exec/resolve_args.py +4 -1
  32. kash/exec/runtime_settings.py +3 -0
  33. kash/file_storage/file_store.py +108 -114
  34. kash/file_storage/item_file_format.py +91 -26
  35. kash/file_storage/item_id_index.py +128 -0
  36. kash/help/help_types.py +1 -1
  37. kash/llm_utils/llms.py +6 -1
  38. kash/local_server/local_server_commands.py +2 -1
  39. kash/mcp/mcp_server_commands.py +3 -2
  40. kash/mcp/mcp_server_routes.py +42 -12
  41. kash/model/actions_model.py +44 -32
  42. kash/model/compound_actions_model.py +4 -3
  43. kash/model/exec_model.py +33 -3
  44. kash/model/items_model.py +150 -60
  45. kash/model/params_model.py +4 -4
  46. kash/shell/output/shell_output.py +1 -2
  47. kash/utils/api_utils/gather_limited.py +2 -0
  48. kash/utils/api_utils/multitask_gather.py +74 -0
  49. kash/utils/common/s3_utils.py +108 -0
  50. kash/utils/common/url.py +16 -4
  51. kash/utils/file_formats/chat_format.py +7 -4
  52. kash/utils/file_utils/file_ext.py +1 -0
  53. kash/utils/file_utils/file_formats.py +4 -2
  54. kash/utils/file_utils/file_formats_model.py +12 -0
  55. kash/utils/text_handling/doc_normalization.py +1 -1
  56. kash/utils/text_handling/markdown_footnotes.py +224 -0
  57. kash/utils/text_handling/markdown_utils.py +532 -41
  58. kash/utils/text_handling/markdownify_utils.py +2 -1
  59. kash/web_content/web_fetch.py +2 -1
  60. kash/web_gen/templates/components/tooltip_scripts.js.jinja +186 -1
  61. kash/web_gen/templates/components/youtube_popover_scripts.js.jinja +223 -0
  62. kash/web_gen/templates/components/youtube_popover_styles.css.jinja +150 -0
  63. kash/web_gen/templates/content_styles.css.jinja +53 -1
  64. kash/web_gen/templates/youtube_webpage.html.jinja +47 -0
  65. kash/web_gen/webpage_render.py +103 -0
  66. kash/workspaces/workspaces.py +0 -5
  67. kash/xonsh_custom/custom_shell.py +4 -3
  68. {kash_shell-0.3.28.dist-info → kash_shell-0.3.33.dist-info}/METADATA +35 -26
  69. {kash_shell-0.3.28.dist-info → kash_shell-0.3.33.dist-info}/RECORD +72 -64
  70. kash/llm_utils/llm_features.py +0 -72
  71. kash/web_gen/simple_webpage.py +0 -55
  72. {kash_shell-0.3.28.dist-info → kash_shell-0.3.33.dist-info}/WHEEL +0 -0
  73. {kash_shell-0.3.28.dist-info → kash_shell-0.3.33.dist-info}/entry_points.txt +0 -0
  74. {kash_shell-0.3.28.dist-info → kash_shell-0.3.33.dist-info}/licenses/LICENSE +0 -0
@@ -293,7 +293,8 @@ def download_url(
293
293
 
294
294
  s3 = boto3.resource("s3")
295
295
  s3_path = parsed_url.path.lstrip("/")
296
- s3.Bucket(parsed_url.netloc).download_file(s3_path, target_filename)
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
- console.debug(`Tooltip initialization complete. Total footnote tooltips: ${footnoteCount}`);
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
- .chunk {
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);