kash-shell 0.3.20__py3-none-any.whl → 0.3.22__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/markdownify_html.py +11 -0
- kash/actions/core/tabbed_webpage_generate.py +2 -2
- kash/commands/help/assistant_commands.py +2 -4
- kash/commands/help/logo.py +12 -17
- kash/commands/help/welcome.py +5 -4
- kash/config/colors.py +8 -6
- kash/config/text_styles.py +2 -0
- kash/docs/markdown/topics/b1_kash_overview.md +34 -45
- kash/docs/markdown/warning.md +3 -3
- kash/docs/markdown/welcome.md +2 -1
- kash/exec/action_decorators.py +20 -5
- kash/exec/fetch_url_items.py +6 -4
- kash/exec/llm_transforms.py +1 -1
- kash/exec/preconditions.py +7 -2
- kash/exec/shell_callable_action.py +1 -1
- kash/llm_utils/llm_completion.py +1 -1
- kash/model/actions_model.py +6 -0
- kash/model/items_model.py +14 -11
- kash/shell/output/shell_output.py +20 -1
- kash/utils/api_utils/api_retries.py +305 -0
- kash/utils/api_utils/cache_requests_limited.py +84 -0
- kash/utils/api_utils/gather_limited.py +987 -0
- kash/utils/api_utils/progress_protocol.py +299 -0
- kash/utils/common/function_inspect.py +66 -1
- kash/utils/common/testing.py +10 -7
- kash/utils/rich_custom/multitask_status.py +631 -0
- kash/utils/text_handling/escape_html_tags.py +16 -11
- kash/utils/text_handling/markdown_render.py +1 -0
- kash/utils/text_handling/markdown_utils.py +158 -1
- kash/web_gen/tabbed_webpage.py +2 -2
- kash/web_gen/templates/base_styles.css.jinja +26 -20
- kash/web_gen/templates/components/toc_styles.css.jinja +1 -1
- kash/web_gen/templates/components/tooltip_scripts.js.jinja +171 -19
- kash/web_gen/templates/components/tooltip_styles.css.jinja +23 -8
- kash/xonsh_custom/load_into_xonsh.py +0 -3
- {kash_shell-0.3.20.dist-info → kash_shell-0.3.22.dist-info}/METADATA +3 -1
- {kash_shell-0.3.20.dist-info → kash_shell-0.3.22.dist-info}/RECORD +40 -35
- {kash_shell-0.3.20.dist-info → kash_shell-0.3.22.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.20.dist-info → kash_shell-0.3.22.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.20.dist-info → kash_shell-0.3.22.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from collections.abc import Set
|
|
3
3
|
|
|
4
|
-
HTML_IN_MD_TAGS = frozenset(
|
|
5
|
-
"""
|
|
4
|
+
HTML_IN_MD_TAGS = frozenset(
|
|
5
|
+
["div", "span", "i", "b", "em", "sup", "sub", "br", "details", "summary"]
|
|
6
|
+
)
|
|
7
|
+
"""
|
|
8
|
+
These are tags that have reasonable usage in Markdown so typically would be preserved.
|
|
9
|
+
Note we want `<i>` because it's used for icons like `<i data-feather="list"></i>`.
|
|
10
|
+
"""
|
|
6
11
|
|
|
7
12
|
ALLOWED_BARE_PROTOS = frozenset(["http://", "https://", "file://"])
|
|
8
13
|
|
|
@@ -68,7 +73,7 @@ def escape_html_tags(
|
|
|
68
73
|
def test_escape_html_tags():
|
|
69
74
|
"""Tests the escape_html_tags function with various cases."""
|
|
70
75
|
|
|
71
|
-
#
|
|
76
|
+
# Basic Whitelist Check (Default)
|
|
72
77
|
assert escape_html_tags("<div>Test</div>") == "<div>Test</div>"
|
|
73
78
|
assert escape_html_tags("<span>Test</span>") == "<span>Test</span>"
|
|
74
79
|
assert escape_html_tags("<br>") == "<br>"
|
|
@@ -77,21 +82,21 @@ def test_escape_html_tags():
|
|
|
77
82
|
== "<details><summary>Sum</summary>Det</details>"
|
|
78
83
|
)
|
|
79
84
|
|
|
80
|
-
#
|
|
85
|
+
# Basic Escape Check
|
|
81
86
|
assert escape_html_tags("<p>Test</p>") == "<p>Test</p>"
|
|
82
87
|
assert escape_html_tags("<script>alert('x');</script>") == "<script>alert('x');</script>"
|
|
83
88
|
assert escape_html_tags("<img>") == "<img>"
|
|
84
89
|
|
|
85
|
-
#
|
|
90
|
+
# Case Insensitivity
|
|
86
91
|
assert escape_html_tags("<DiV>Case</DiV>") == "<DiV>Case</DiV>" # Whitelisted
|
|
87
92
|
assert escape_html_tags("<P>Test</P>") == "<P>Test</P>" # Escaped
|
|
88
93
|
|
|
89
|
-
#
|
|
94
|
+
# Self-closing tags
|
|
90
95
|
assert escape_html_tags("<br/>") == "<br/>" # Whitelisted
|
|
91
96
|
assert escape_html_tags("<br />") == "<br />" # Whitelisted
|
|
92
97
|
assert escape_html_tags("<img/>") == "<img/>" # Escaped
|
|
93
98
|
|
|
94
|
-
#
|
|
99
|
+
# Tags with Attributes
|
|
95
100
|
assert (
|
|
96
101
|
escape_html_tags('<div class="foo">Test</div>') == '<div class="foo">Test</div>'
|
|
97
102
|
) # Whitelisted
|
|
@@ -102,7 +107,7 @@ def test_escape_html_tags():
|
|
|
102
107
|
assert escape_html_tags('<p class="foo">Test</p>') == '<p class="foo">Test</p>' # Escaped
|
|
103
108
|
assert escape_html_tags('<img src="a.jpg"/>') == '<img src="a.jpg"/>' # Escaped
|
|
104
109
|
|
|
105
|
-
#
|
|
110
|
+
# Markdown URL Handling
|
|
106
111
|
url_md = "Check <https://example.com> and <http://test.org/path>"
|
|
107
112
|
assert escape_html_tags(url_md, allow_bare_md_urls=True) == url_md
|
|
108
113
|
assert (
|
|
@@ -127,7 +132,7 @@ def test_escape_html_tags():
|
|
|
127
132
|
== "</https://example.com>"
|
|
128
133
|
) # Closing URL-like is escaped
|
|
129
134
|
|
|
130
|
-
#
|
|
135
|
+
# Nested/Malformed '<' and Edge Cases
|
|
131
136
|
assert escape_html_tags("<<script>>") == "<<script>>" # Escaped non-tag <
|
|
132
137
|
assert escape_html_tags("<div><p>nested</p></div>") == "<div><p>nested</p></div>"
|
|
133
138
|
assert escape_html_tags("<div<span") == "<div<span" # Incomplete tags are escaped
|
|
@@ -140,7 +145,7 @@ def test_escape_html_tags():
|
|
|
140
145
|
assert escape_html_tags("< >") == "< >"
|
|
141
146
|
assert escape_html_tags("< / div >") == "< / div >" # Whitelisted closing tag with spaces
|
|
142
147
|
|
|
143
|
-
#
|
|
148
|
+
# Mixed Content Combination
|
|
144
149
|
complex_html = "<DiV class='A'>Hello <Br/> <p>World</p> <https://link.com> </DiV>"
|
|
145
150
|
expected_complex_allowed = (
|
|
146
151
|
"<DiV class='A'>Hello <Br/> <p>World</p> <https://link.com> </DiV>"
|
|
@@ -151,6 +156,6 @@ def test_escape_html_tags():
|
|
|
151
156
|
assert escape_html_tags(complex_html, allow_bare_md_urls=True) == expected_complex_allowed
|
|
152
157
|
assert escape_html_tags(complex_html, allow_bare_md_urls=False) == expected_complex_disallowed
|
|
153
158
|
|
|
154
|
-
#
|
|
159
|
+
# Empty/No Tags
|
|
155
160
|
assert escape_html_tags("") == ""
|
|
156
161
|
assert escape_html_tags("Just plain text, no tags.") == "Just plain text, no tags."
|
|
@@ -6,7 +6,7 @@ from typing import Any, TypeAlias
|
|
|
6
6
|
import marko
|
|
7
7
|
import regex
|
|
8
8
|
from marko.block import Heading, ListItem
|
|
9
|
-
from marko.inline import Link
|
|
9
|
+
from marko.inline import AutoLink, Link
|
|
10
10
|
|
|
11
11
|
from kash.utils.common.url import Url
|
|
12
12
|
|
|
@@ -64,6 +64,9 @@ def _tree_links(element, include_internal=False):
|
|
|
64
64
|
case Link():
|
|
65
65
|
if include_internal or not element.dest.startswith("#"):
|
|
66
66
|
links.append(element.dest)
|
|
67
|
+
case AutoLink():
|
|
68
|
+
if include_internal or not element.dest.startswith("#"):
|
|
69
|
+
links.append(element.dest)
|
|
67
70
|
case _:
|
|
68
71
|
if hasattr(element, "children"):
|
|
69
72
|
for child in element.children:
|
|
@@ -710,3 +713,157 @@ def test_markdown_utils_exceptions() -> None:
|
|
|
710
713
|
result_with_internal = extract_links(content, include_internal=True)
|
|
711
714
|
assert "https://example.com" in result_with_internal
|
|
712
715
|
assert "#section" in result_with_internal
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def test_extract_links_comprehensive() -> None:
|
|
719
|
+
"""Test extract_links with various link formats including bare links and footnotes."""
|
|
720
|
+
|
|
721
|
+
# Test regular markdown links
|
|
722
|
+
regular_links = "Check out [this link](https://example.com) and [another](https://test.com)"
|
|
723
|
+
result = extract_links(regular_links)
|
|
724
|
+
assert "https://example.com" in result
|
|
725
|
+
assert "https://test.com" in result
|
|
726
|
+
assert len(result) == 2
|
|
727
|
+
|
|
728
|
+
# Test bare/autolinks in angle brackets
|
|
729
|
+
bare_links = "Visit <https://google.com> and also <https://github.com>"
|
|
730
|
+
result_bare = extract_links(bare_links)
|
|
731
|
+
assert "https://google.com" in result_bare
|
|
732
|
+
assert "https://github.com" in result_bare
|
|
733
|
+
assert len(result_bare) == 2
|
|
734
|
+
|
|
735
|
+
# Test autolinks without brackets (expected to not work with standard markdown)
|
|
736
|
+
auto_links = "Visit https://stackoverflow.com or http://reddit.com"
|
|
737
|
+
result_auto = extract_links(auto_links)
|
|
738
|
+
assert (
|
|
739
|
+
result_auto == []
|
|
740
|
+
) # Plain URLs without brackets aren't parsed as links in standard markdown
|
|
741
|
+
|
|
742
|
+
# Test GFM footnotes (the original issue)
|
|
743
|
+
footnote_content = """
|
|
744
|
+
[^109]: What Is The Future Of Ketamine Therapy For Mental Health Treatment?
|
|
745
|
+
- The Ko-Op, accessed June 28, 2025,
|
|
746
|
+
<https://psychedelictherapists.co/blog/the-future-of-ketamine-assisted-psychotherapy/>
|
|
747
|
+
"""
|
|
748
|
+
result_footnote = extract_links(footnote_content)
|
|
749
|
+
assert (
|
|
750
|
+
"https://psychedelictherapists.co/blog/the-future-of-ketamine-assisted-psychotherapy/"
|
|
751
|
+
in result_footnote
|
|
752
|
+
)
|
|
753
|
+
assert len(result_footnote) == 1
|
|
754
|
+
|
|
755
|
+
# Test mixed content with all types (excluding reference-style which has parsing conflicts with footnotes)
|
|
756
|
+
mixed_content = """
|
|
757
|
+
# Header
|
|
758
|
+
|
|
759
|
+
Regular link: [Example](https://example.com)
|
|
760
|
+
Bare link: <https://bare-link.com>
|
|
761
|
+
Auto link: https://auto-link.com
|
|
762
|
+
|
|
763
|
+
[^1]: Footnote with [regular link](https://footnote-regular.com)
|
|
764
|
+
[^2]: Footnote with bare link <https://footnote-bare.com>
|
|
765
|
+
"""
|
|
766
|
+
result_mixed = extract_links(mixed_content)
|
|
767
|
+
expected_links = [
|
|
768
|
+
"https://example.com", # Regular link
|
|
769
|
+
"https://bare-link.com", # Bare link
|
|
770
|
+
"https://footnote-regular.com", # Link in footnote
|
|
771
|
+
"https://footnote-bare.com", # Bare link in footnote
|
|
772
|
+
]
|
|
773
|
+
for link in expected_links:
|
|
774
|
+
assert link in result_mixed, f"Missing expected link: {link}"
|
|
775
|
+
# Should not include plain auto link (https://auto-link.com) as it's not in angle brackets
|
|
776
|
+
assert "https://auto-link.com" not in result_mixed
|
|
777
|
+
assert len(result_mixed) == len(expected_links)
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def test_extract_bare_links() -> None:
|
|
781
|
+
"""Test extraction of bare links in angle brackets."""
|
|
782
|
+
content = "Visit <https://example.com> and <https://github.com/user/repo> for more info"
|
|
783
|
+
result = extract_links(content)
|
|
784
|
+
assert "https://example.com" in result
|
|
785
|
+
assert "https://github.com/user/repo" in result
|
|
786
|
+
assert len(result) == 2
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def test_extract_footnote_links() -> None:
|
|
790
|
+
"""Test extraction of links within footnotes."""
|
|
791
|
+
content = dedent("""
|
|
792
|
+
Main text with reference[^1].
|
|
793
|
+
|
|
794
|
+
[^1]: This footnote has a [regular link](https://example.com) and <https://bare-link.com>
|
|
795
|
+
""")
|
|
796
|
+
result = extract_links(content)
|
|
797
|
+
assert "https://example.com" in result
|
|
798
|
+
assert "https://bare-link.com" in result
|
|
799
|
+
assert len(result) == 2
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def test_extract_reference_style_links() -> None:
|
|
803
|
+
"""Test extraction of reference-style links."""
|
|
804
|
+
content = dedent("""
|
|
805
|
+
Check out [this article][ref1] and [this other one][ref2].
|
|
806
|
+
|
|
807
|
+
[ref1]: https://example.com/article1
|
|
808
|
+
[ref2]: https://example.com/article2
|
|
809
|
+
""")
|
|
810
|
+
result = extract_links(content)
|
|
811
|
+
assert "https://example.com/article1" in result
|
|
812
|
+
assert "https://example.com/article2" in result
|
|
813
|
+
assert len(result) == 2
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def test_extract_links_with_internal_fragments() -> None:
|
|
817
|
+
"""Test that internal fragment links are excluded by default but included when requested."""
|
|
818
|
+
content = dedent("""
|
|
819
|
+
See [this section](#introduction) and [external link](https://example.com).
|
|
820
|
+
Also check [another section](#conclusion) here.
|
|
821
|
+
""")
|
|
822
|
+
|
|
823
|
+
# Default behavior: exclude internal links
|
|
824
|
+
result = extract_links(content)
|
|
825
|
+
assert "https://example.com" in result
|
|
826
|
+
assert "#introduction" not in result
|
|
827
|
+
assert "#conclusion" not in result
|
|
828
|
+
assert len(result) == 1
|
|
829
|
+
|
|
830
|
+
# Include internal links
|
|
831
|
+
result_with_internal = extract_links(content, include_internal=True)
|
|
832
|
+
assert "https://example.com" in result_with_internal
|
|
833
|
+
assert "#introduction" in result_with_internal
|
|
834
|
+
assert "#conclusion" in result_with_internal
|
|
835
|
+
assert len(result_with_internal) == 3
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def test_extract_links_mixed_real_world() -> None:
|
|
839
|
+
"""Test with a realistic mixed document containing various link types."""
|
|
840
|
+
content = dedent("""
|
|
841
|
+
# Research Article
|
|
842
|
+
|
|
843
|
+
This study examines ketamine therapy[^109] and references multiple sources.
|
|
844
|
+
|
|
845
|
+
## Methods
|
|
846
|
+
|
|
847
|
+
We reviewed literature from [PubMed](https://pubmed.ncbi.nlm.nih.gov)
|
|
848
|
+
and other databases <https://scholar.google.com>.
|
|
849
|
+
|
|
850
|
+
For protocol details, see [our methodology][methodology].
|
|
851
|
+
|
|
852
|
+
[methodology]: https://research.example.com/protocol
|
|
853
|
+
|
|
854
|
+
[^109]: What Is The Future Of Ketamine Therapy For Mental Health Treatment?
|
|
855
|
+
- The Ko-Op, accessed June 28, 2025,
|
|
856
|
+
<https://psychedelictherapists.co/blog/the-future-of-ketamine-assisted-psychotherapy/>
|
|
857
|
+
""")
|
|
858
|
+
|
|
859
|
+
result = extract_links(content)
|
|
860
|
+
expected_links = [
|
|
861
|
+
"https://pubmed.ncbi.nlm.nih.gov",
|
|
862
|
+
"https://scholar.google.com",
|
|
863
|
+
"https://research.example.com/protocol",
|
|
864
|
+
"https://psychedelictherapists.co/blog/the-future-of-ketamine-assisted-psychotherapy/",
|
|
865
|
+
]
|
|
866
|
+
|
|
867
|
+
for link in expected_links:
|
|
868
|
+
assert link in result, f"Missing expected link: {link}"
|
|
869
|
+
assert len(result) == len(expected_links)
|
kash/web_gen/tabbed_webpage.py
CHANGED
|
@@ -82,7 +82,7 @@ def tabbed_webpage_config(
|
|
|
82
82
|
|
|
83
83
|
config_item = Item(
|
|
84
84
|
title=f"{title} (config)",
|
|
85
|
-
type=ItemType.
|
|
85
|
+
type=ItemType.data,
|
|
86
86
|
format=Format.yaml,
|
|
87
87
|
body=to_yaml_string(asdict(config)),
|
|
88
88
|
)
|
|
@@ -108,7 +108,7 @@ def tabbed_webpage_generate(
|
|
|
108
108
|
"""
|
|
109
109
|
Generate a web page using the supplied config.
|
|
110
110
|
"""
|
|
111
|
-
config = config_item.
|
|
111
|
+
config = config_item.read_as_data()
|
|
112
112
|
tabbed_webpage = as_dataclass(config, TabbedWebpage) # Checks the format.
|
|
113
113
|
|
|
114
114
|
_load_tab_content(tabbed_webpage)
|
|
@@ -5,7 +5,7 @@
|
|
|
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-medium:
|
|
8
|
+
--font-weight-sans-medium: 550;
|
|
9
9
|
--font-weight-sans-bold: 650;
|
|
10
10
|
--font-mono: "Hack Nerd Font", "Menlo", "DejaVu Sans Mono", Consolas, "Lucida Console", monospace;
|
|
11
11
|
|
|
@@ -156,7 +156,13 @@ h4 {
|
|
|
156
156
|
margin-bottom: 0.7rem;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
h5
|
|
159
|
+
h5 {
|
|
160
|
+
font-size: 1rem;
|
|
161
|
+
margin-top: 0.7rem;
|
|
162
|
+
margin-bottom: 0.5rem;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
h6 {
|
|
160
166
|
font-size: 1rem;
|
|
161
167
|
margin-top: 0.7rem;
|
|
162
168
|
margin-bottom: 0.5rem;
|
|
@@ -368,15 +374,15 @@ hr:before {
|
|
|
368
374
|
|
|
369
375
|
.long-text h3 {
|
|
370
376
|
font-family: var(--font-sans);
|
|
371
|
-
font-weight:
|
|
377
|
+
font-weight: 550;
|
|
372
378
|
text-transform: uppercase;
|
|
373
379
|
letter-spacing: 0.025em;
|
|
374
380
|
}
|
|
375
381
|
|
|
376
382
|
.long-text h4 {
|
|
377
|
-
font-family: var(--font-
|
|
378
|
-
font-weight:
|
|
379
|
-
|
|
383
|
+
font-family: var(--font-serif);
|
|
384
|
+
font-weight: 400;
|
|
385
|
+
font-style: italic;
|
|
380
386
|
}
|
|
381
387
|
|
|
382
388
|
.long-text h5 {
|
|
@@ -435,6 +441,10 @@ hr:before {
|
|
|
435
441
|
{% endblock long_text_styles %}
|
|
436
442
|
|
|
437
443
|
{% block table_styles %}
|
|
444
|
+
table, th, td, tbody tr {
|
|
445
|
+
transition: background-color 0.4s ease-in-out, border-color 0.4s ease-in-out;
|
|
446
|
+
}
|
|
447
|
+
|
|
438
448
|
table {
|
|
439
449
|
font-family: var(--font-sans);
|
|
440
450
|
font-size: var(--font-size-small);
|
|
@@ -453,6 +463,7 @@ th {
|
|
|
453
463
|
letter-spacing: 0.03em;
|
|
454
464
|
border-bottom: 1px solid var(--color-border-hint);
|
|
455
465
|
line-height: 1.2;
|
|
466
|
+
background-color: var(--color-bg-alt-solid);
|
|
456
467
|
}
|
|
457
468
|
|
|
458
469
|
th, td {
|
|
@@ -461,10 +472,6 @@ th, td {
|
|
|
461
472
|
min-width: 6rem;
|
|
462
473
|
}
|
|
463
474
|
|
|
464
|
-
th {
|
|
465
|
-
background-color: var(--color-bg-alt-solid);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
475
|
tbody tr:nth-child(even) {
|
|
469
476
|
background-color: var(--color-bg-alt-solid);
|
|
470
477
|
}
|
|
@@ -480,7 +487,12 @@ tbody tr:nth-child(even) {
|
|
|
480
487
|
transform: translateX(-50%);
|
|
481
488
|
/* Prevent container from expanding beyond its content area */
|
|
482
489
|
overflow-x: auto;
|
|
483
|
-
overflow-y:
|
|
490
|
+
overflow-y: hidden; /* Whole height of table shown, no vertical scrolling. */
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.table-container table {
|
|
494
|
+
/* Tricky: Need this to prevent bogus extra horizontal scroll while keeping normal sizing */
|
|
495
|
+
contain: content;
|
|
484
496
|
}
|
|
485
497
|
|
|
486
498
|
/* When TOC is present, simplify table container positioning */
|
|
@@ -540,7 +552,6 @@ sup {
|
|
|
540
552
|
width: calc(100vw - 6rem);
|
|
541
553
|
/* Ensure container doesn't expand beyond its width */
|
|
542
554
|
max-width: calc(100vw - 6rem);
|
|
543
|
-
contain: layout inline-size;
|
|
544
555
|
}
|
|
545
556
|
|
|
546
557
|
/* Apply shadow to long-text containers on larger screens */
|
|
@@ -590,8 +601,6 @@ sup {
|
|
|
590
601
|
|
|
591
602
|
/* Ensure horizontal scroll works properly without expanding container */
|
|
592
603
|
overflow-x: auto;
|
|
593
|
-
overflow-y: visible;
|
|
594
|
-
contain: layout inline-size;
|
|
595
604
|
}
|
|
596
605
|
|
|
597
606
|
.content-with-toc.has-toc table {
|
|
@@ -634,20 +643,17 @@ sup {
|
|
|
634
643
|
|
|
635
644
|
/* Make table containers scrollable without affecting page layout */
|
|
636
645
|
.table-container {
|
|
637
|
-
width: calc(100vw - 3rem); /* Fixed width instead of max-width */
|
|
638
|
-
max-width: calc(100vw - 3rem);
|
|
639
646
|
overflow-x: auto;
|
|
640
|
-
overflow-y: visible; /* Ensure vertical overflow is not hidden */
|
|
641
647
|
transform: none;
|
|
642
648
|
left: 0;
|
|
643
649
|
position: relative;
|
|
644
650
|
margin-left: auto;
|
|
645
|
-
margin-right:
|
|
651
|
+
margin-right: 0; /* Extend to right edge */
|
|
646
652
|
/* Prevent container from expanding beyond its width */
|
|
647
653
|
box-sizing: border-box;
|
|
648
|
-
|
|
654
|
+
width: calc(100vw - 1.5rem); /* Full width minus left margin */
|
|
649
655
|
}
|
|
650
|
-
|
|
656
|
+
|
|
651
657
|
table {
|
|
652
658
|
font-size: var(--font-size-smaller);
|
|
653
659
|
/* Tables can be wider than container on mobile */
|