kash-shell 0.3.25__py3-none-any.whl → 0.3.26__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/commands/help/assistant_commands.py +4 -3
- kash/config/colors.py +5 -3
- kash/config/text_styles.py +1 -0
- kash/config/unified_live.py +251 -0
- kash/docs/markdown/assistant_instructions_template.md +3 -3
- kash/docs/markdown/topics/a1_what_is_kash.md +22 -20
- kash/docs/markdown/topics/a2_installation.md +10 -10
- kash/docs/markdown/topics/a3_getting_started.md +8 -8
- kash/docs/markdown/topics/a4_elements.md +3 -3
- kash/docs/markdown/topics/a5_tips_for_use_with_other_tools.md +12 -12
- kash/docs/markdown/topics/b0_philosophy_of_kash.md +17 -17
- kash/docs/markdown/topics/b1_kash_overview.md +7 -7
- kash/docs/markdown/topics/b2_workspace_and_file_formats.md +1 -1
- kash/docs/markdown/topics/b3_modern_shell_tool_recommendations.md +1 -1
- kash/docs/markdown/topics/b4_faq.md +7 -7
- kash/docs/markdown/welcome.md +1 -1
- kash/embeddings/embeddings.py +110 -39
- kash/embeddings/text_similarity.py +2 -2
- kash/exec/shell_callable_action.py +4 -3
- kash/help/help_embeddings.py +5 -2
- kash/model/graph_model.py +2 -0
- kash/model/items_model.py +3 -3
- kash/shell/output/shell_output.py +2 -2
- kash/utils/file_utils/csv_utils.py +105 -0
- kash/utils/rich_custom/multitask_status.py +19 -5
- kash/web_gen/templates/base_styles.css.jinja +348 -23
- kash/web_gen/templates/base_webpage.html.jinja +11 -0
- kash/web_gen/templates/components/toc_styles.css.jinja +15 -3
- kash/web_gen/templates/components/tooltip_styles.css.jinja +1 -0
- kash/web_gen/templates/content_styles.css.jinja +23 -9
- kash/web_gen/templates/item_view.html.jinja +12 -4
- kash/web_gen/templates/simple_webpage.html.jinja +2 -2
- kash/xonsh_custom/custom_shell.py +7 -4
- {kash_shell-0.3.25.dist-info → kash_shell-0.3.26.dist-info}/METADATA +58 -55
- {kash_shell-0.3.25.dist-info → kash_shell-0.3.26.dist-info}/RECORD +38 -36
- {kash_shell-0.3.25.dist-info → kash_shell-0.3.26.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.25.dist-info → kash_shell-0.3.26.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.25.dist-info → kash_shell-0.3.26.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import NamedTuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CsvMetadata(NamedTuple):
|
|
9
|
+
"""
|
|
10
|
+
Result of CSV analysis, containing skip rows, metadata, and dialect.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
skip_rows: int
|
|
14
|
+
metadata: dict[str, str]
|
|
15
|
+
dialect: type[csv.Dialect]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def sniff_csv_metadata(
|
|
19
|
+
file_path: Path,
|
|
20
|
+
*,
|
|
21
|
+
max_scan_lines: int = 500,
|
|
22
|
+
threshold_ratio: float = 0.8,
|
|
23
|
+
min_columns: int = 3,
|
|
24
|
+
sample_size: int = 32768,
|
|
25
|
+
) -> CsvMetadata:
|
|
26
|
+
"""
|
|
27
|
+
Detect CSV metadata and where the table data starts by finding the first row that looks
|
|
28
|
+
like proper headers.
|
|
29
|
+
|
|
30
|
+
This function handles various CSV formats:
|
|
31
|
+
- Normal CSV files: returns skip_rows=0 (no rows to skip)
|
|
32
|
+
- Files with metadata: detects the first row with multiple columns that looks like headers
|
|
33
|
+
- Survey exports: handles key-value metadata followed by proper CSV structure
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
file_path: Path to the CSV file to analyze
|
|
37
|
+
max_scan_lines: Maximum number of lines to scan before giving up
|
|
38
|
+
threshold_ratio: Minimum ratio of max columns a row must have to be considered headers
|
|
39
|
+
min_columns: Minimum number of columns required to be considered headers
|
|
40
|
+
sample_size: Number of bytes to read for dialect detection
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
CsvMetadata with skip_rows, metadata dict, and detected dialect
|
|
44
|
+
"""
|
|
45
|
+
# Read sample for dialect detection
|
|
46
|
+
sample_text = file_path.read_text(encoding="utf-8", errors="replace")[:sample_size]
|
|
47
|
+
|
|
48
|
+
# Detect CSV dialect
|
|
49
|
+
try:
|
|
50
|
+
dialect = csv.Sniffer().sniff(sample_text)
|
|
51
|
+
except csv.Error:
|
|
52
|
+
# Fall back to default dialect if detection fails
|
|
53
|
+
dialect = csv.excel
|
|
54
|
+
|
|
55
|
+
# Analyze file structure
|
|
56
|
+
with open(file_path, encoding="utf-8", errors="replace") as file:
|
|
57
|
+
reader = csv.reader(file, dialect=dialect)
|
|
58
|
+
|
|
59
|
+
max_columns = 0
|
|
60
|
+
header_candidates = []
|
|
61
|
+
metadata = {}
|
|
62
|
+
|
|
63
|
+
for line_num, row in enumerate(reader):
|
|
64
|
+
# Stop scanning if we've looked at too many lines
|
|
65
|
+
if line_num >= max_scan_lines:
|
|
66
|
+
break
|
|
67
|
+
|
|
68
|
+
# Skip completely empty rows
|
|
69
|
+
non_empty_cells = [cell.strip() for cell in row if cell.strip()]
|
|
70
|
+
if not non_empty_cells:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
column_count = len(non_empty_cells)
|
|
74
|
+
|
|
75
|
+
# Track the maximum number of columns seen
|
|
76
|
+
if column_count > max_columns:
|
|
77
|
+
max_columns = column_count
|
|
78
|
+
|
|
79
|
+
# Collect potential key-value metadata (exactly 2 columns)
|
|
80
|
+
# Only collect metadata before we find any header candidates with min_columns
|
|
81
|
+
if column_count == 2 and not any(hc[1] >= min_columns for hc in header_candidates):
|
|
82
|
+
key, value = non_empty_cells[0], non_empty_cells[1]
|
|
83
|
+
# Simple heuristic: if it looks like a key-value pair, store it
|
|
84
|
+
if not key.isdigit() and not value.replace(".", "").replace(",", "").isdigit():
|
|
85
|
+
metadata[key] = value
|
|
86
|
+
|
|
87
|
+
# Consider this a potential header if it has minimum required columns
|
|
88
|
+
if column_count >= min_columns:
|
|
89
|
+
header_candidates.append((line_num, column_count, row))
|
|
90
|
+
|
|
91
|
+
# If no multi-column rows found, assume it's a normal CSV starting at line 0
|
|
92
|
+
if not header_candidates:
|
|
93
|
+
return CsvMetadata(skip_rows=0, metadata=metadata, dialect=dialect)
|
|
94
|
+
|
|
95
|
+
# Look for the first row that has close to the maximum number of columns
|
|
96
|
+
# This helps distinguish metadata (usually fewer columns) from real headers (many columns)
|
|
97
|
+
threshold = max(min_columns, max_columns * threshold_ratio)
|
|
98
|
+
|
|
99
|
+
for line_num, column_count, _row in header_candidates:
|
|
100
|
+
if column_count >= threshold:
|
|
101
|
+
return CsvMetadata(skip_rows=line_num, metadata=metadata, dialect=dialect)
|
|
102
|
+
|
|
103
|
+
# If no clear header found but we have candidates, return the first multi-column row
|
|
104
|
+
first_candidate_line = header_candidates[0][0]
|
|
105
|
+
return CsvMetadata(skip_rows=first_candidate_line, metadata=metadata, dialect=dialect)
|
|
@@ -4,7 +4,7 @@ import asyncio
|
|
|
4
4
|
from contextlib import AbstractAsyncContextManager
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from types import TracebackType
|
|
7
|
-
from typing import TYPE_CHECKING, TypeVar
|
|
7
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
8
8
|
|
|
9
9
|
from strif import abbrev_str, single_line
|
|
10
10
|
from typing_extensions import override
|
|
@@ -17,6 +17,7 @@ from rich.progress import BarColumn, Progress, ProgressColumn, Task, TaskID
|
|
|
17
17
|
from rich.spinner import Spinner
|
|
18
18
|
from rich.text import Text
|
|
19
19
|
|
|
20
|
+
from kash.config.unified_live import get_unified_live
|
|
20
21
|
from kash.utils.api_utils.progress_protocol import (
|
|
21
22
|
EMOJI_FAILURE,
|
|
22
23
|
EMOJI_RETRY,
|
|
@@ -229,7 +230,7 @@ class TruncatedLabelColumn(ProgressColumn):
|
|
|
229
230
|
def __init__(self, console_width: int):
|
|
230
231
|
super().__init__()
|
|
231
232
|
# Reserve half the console width for labels/status messages
|
|
232
|
-
self.max_label_width = console_width // 2
|
|
233
|
+
self.max_label_width: int = console_width // 2
|
|
233
234
|
|
|
234
235
|
@override
|
|
235
236
|
def render(self, task: Task) -> Text:
|
|
@@ -298,6 +299,9 @@ class MultiTaskStatus(AbstractAsyncContextManager):
|
|
|
298
299
|
self._next_id: int = 1
|
|
299
300
|
self._rich_task_ids: dict[int, TaskID] = {} # Map our IDs to Rich Progress IDs
|
|
300
301
|
|
|
302
|
+
# Unified live integration
|
|
303
|
+
self._unified_live: Any | None = None # Reference to the global unified live
|
|
304
|
+
|
|
301
305
|
# Calculate spinner width for consistent spacing
|
|
302
306
|
self._spinner_width = _get_spinner_width(SPINNER_NAME)
|
|
303
307
|
|
|
@@ -367,7 +371,13 @@ class MultiTaskStatus(AbstractAsyncContextManager):
|
|
|
367
371
|
@override
|
|
368
372
|
async def __aenter__(self) -> MultiTaskStatus:
|
|
369
373
|
"""Start the live display."""
|
|
370
|
-
|
|
374
|
+
# Try to integrate with unified live display
|
|
375
|
+
|
|
376
|
+
# Always integrate with unified live display (auto-initialized)
|
|
377
|
+
unified_live = get_unified_live()
|
|
378
|
+
self._unified_live = unified_live
|
|
379
|
+
# Register our progress display with the unified live
|
|
380
|
+
unified_live.set_multitask_display(self._progress)
|
|
371
381
|
return self
|
|
372
382
|
|
|
373
383
|
@override
|
|
@@ -378,9 +388,13 @@ class MultiTaskStatus(AbstractAsyncContextManager):
|
|
|
378
388
|
exc_tb: TracebackType | None,
|
|
379
389
|
) -> None:
|
|
380
390
|
"""Stop the live display and show automatic summary if enabled."""
|
|
381
|
-
|
|
391
|
+
# Always clean up unified live integration
|
|
392
|
+
if self._unified_live is not None:
|
|
393
|
+
# Remove our display from the unified live
|
|
394
|
+
self._unified_live.set_multitask_display(None)
|
|
395
|
+
self._unified_live = None
|
|
382
396
|
|
|
383
|
-
# Show automatic summary if enabled
|
|
397
|
+
# Show automatic summary if enabled (always print to console now)
|
|
384
398
|
if self.auto_summary:
|
|
385
399
|
summary = self.get_summary()
|
|
386
400
|
self.console.print(summary)
|
|
@@ -3,12 +3,22 @@
|
|
|
3
3
|
font-size: 16px;
|
|
4
4
|
/* Adding Hack Nerd Font to all fonts for icon support, if it is installed. */
|
|
5
5
|
--font-sans: "Source Sans 3 Variable", sans-serif, "Hack Nerd Font";
|
|
6
|
-
--font-serif: "PT Serif", serif, "Hack Nerd Font";
|
|
6
|
+
--font-serif: "LocalPunct", "PT Serif", serif, "Hack Nerd Font";
|
|
7
7
|
/* Source Sans 3 Variable better at these weights. */
|
|
8
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
|
|
|
12
|
+
--font-features-sans: normal;
|
|
13
|
+
{#
|
|
14
|
+
/* TODO: FontSource builds don't seem to support these. Might be nice to use. */
|
|
15
|
+
--font-features-sans:
|
|
16
|
+
"ss01" 0, /* Flat-tailed l */
|
|
17
|
+
"ss02" 1, /* Single-storey a */
|
|
18
|
+
"ss03" 0, /* Single-storey g */
|
|
19
|
+
"ss04" 0; /* Sans-bar I */
|
|
20
|
+
#}
|
|
21
|
+
|
|
12
22
|
--font-size-large: 1.2rem;
|
|
13
23
|
--font-size-normal: 1rem;
|
|
14
24
|
--font-size-small: 0.95rem;
|
|
@@ -17,6 +27,21 @@
|
|
|
17
27
|
--font-size-mono-small: 0.75rem;
|
|
18
28
|
--font-size-mono-tiny: 0.7rem;
|
|
19
29
|
|
|
30
|
+
/* Both Source Sans 3 and PT Serif have small caps, so we use this instead of text-transform. */
|
|
31
|
+
--caps-transform: none;
|
|
32
|
+
--caps-caps-variant: all-small-caps;
|
|
33
|
+
--caps-spacing: 0.025em;
|
|
34
|
+
/* Compensate for small caps (Source Sans small caps are quite small) */
|
|
35
|
+
--caps-heading-size-multiplier: 1.42;
|
|
36
|
+
--caps-heading-line-height: calc(1.2 / var(--caps-heading-size-multiplier));
|
|
37
|
+
|
|
38
|
+
{#
|
|
39
|
+
/* Option to handle small caps manually. */
|
|
40
|
+
--caps-transform: uppercase;
|
|
41
|
+
--caps-caps-variant: none;
|
|
42
|
+
--caps-spacing: 0.025em;
|
|
43
|
+
#}
|
|
44
|
+
|
|
20
45
|
--console-char-width: 88;
|
|
21
46
|
--console-width: calc(var(--console-char-width) + 2rem);
|
|
22
47
|
{% endblock root_variables %}
|
|
@@ -116,12 +141,7 @@ a:hover {
|
|
|
116
141
|
transition: all 0.15s ease-in-out;
|
|
117
142
|
}
|
|
118
143
|
|
|
119
|
-
h1,
|
|
120
|
-
h2,
|
|
121
|
-
h3,
|
|
122
|
-
h4,
|
|
123
|
-
h5,
|
|
124
|
-
h6 {
|
|
144
|
+
h1, h2, h3, h4, h5, h6 {
|
|
125
145
|
line-height: 1.2;
|
|
126
146
|
}
|
|
127
147
|
|
|
@@ -132,7 +152,7 @@ h1 {
|
|
|
132
152
|
}
|
|
133
153
|
|
|
134
154
|
h2 {
|
|
135
|
-
font-size: 1.
|
|
155
|
+
font-size: 1.32rem;
|
|
136
156
|
margin-top: 2rem;
|
|
137
157
|
margin-bottom: 1rem;
|
|
138
158
|
}
|
|
@@ -152,7 +172,7 @@ h3 {
|
|
|
152
172
|
}
|
|
153
173
|
|
|
154
174
|
h4 {
|
|
155
|
-
font-size: 1.
|
|
175
|
+
font-size: 1.12rem;
|
|
156
176
|
margin-top: 1rem;
|
|
157
177
|
margin-bottom: 0.7rem;
|
|
158
178
|
}
|
|
@@ -301,13 +321,17 @@ img {
|
|
|
301
321
|
|
|
302
322
|
details {
|
|
303
323
|
font-family: var(--font-sans);
|
|
324
|
+
font-feature-settings: var(--font-features-sans);
|
|
304
325
|
color: var(--color-text);
|
|
305
326
|
|
|
306
327
|
border: 1px solid var(--color-hint-gentle);
|
|
307
|
-
border-radius: 3px;
|
|
308
328
|
margin: 0.75rem 0;
|
|
309
329
|
}
|
|
310
330
|
|
|
331
|
+
details > :not(summary) {
|
|
332
|
+
padding: 0 0.75rem;
|
|
333
|
+
}
|
|
334
|
+
|
|
311
335
|
summary {
|
|
312
336
|
color: var(--color-secondary);
|
|
313
337
|
padding: .5rem 1rem;
|
|
@@ -317,9 +341,12 @@ summary {
|
|
|
317
341
|
transition: all 0.15s ease-in-out;
|
|
318
342
|
}
|
|
319
343
|
|
|
344
|
+
summary::marker {
|
|
345
|
+
font-size: 0.85rem;
|
|
346
|
+
}
|
|
347
|
+
|
|
320
348
|
summary:hover {
|
|
321
349
|
color: var(--color-primary-light);
|
|
322
|
-
{# background: var(--color-hover-bg); #}
|
|
323
350
|
}
|
|
324
351
|
|
|
325
352
|
/* keep the border on the summary when open so it blends */
|
|
@@ -332,6 +359,36 @@ summary:focus-visible {
|
|
|
332
359
|
outline-offset: 2px;
|
|
333
360
|
}
|
|
334
361
|
|
|
362
|
+
/* Special formatting for document metadata details */
|
|
363
|
+
details.metadata {
|
|
364
|
+
color: var(--color-tertiary);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
details.metadata > :not(summary) {
|
|
368
|
+
color: var(--color-secondary);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
details.metadata summary {
|
|
372
|
+
font-weight: 550;
|
|
373
|
+
font-size: calc(var(--font-size-small) * var(--caps-heading-size-multiplier));
|
|
374
|
+
line-height: var(--caps-heading-line-height);
|
|
375
|
+
text-transform: var(--caps-transform);
|
|
376
|
+
font-variant-caps: var(--caps-caps-variant);
|
|
377
|
+
letter-spacing: var(--caps-spacing);
|
|
378
|
+
color: var(--color-tertiary);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
details.metadata summary:hover {
|
|
382
|
+
color: var(--color-primary-light);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
details.metadata blockquote {
|
|
386
|
+
border-left: none;
|
|
387
|
+
margin: 0 0.75rem;
|
|
388
|
+
font-size: var(--font-size-small);
|
|
389
|
+
font-style: italic;
|
|
390
|
+
}
|
|
391
|
+
|
|
335
392
|
|
|
336
393
|
hr {
|
|
337
394
|
border: none;
|
|
@@ -375,15 +432,21 @@ hr:before {
|
|
|
375
432
|
|
|
376
433
|
.long-text h3 {
|
|
377
434
|
font-family: var(--font-sans);
|
|
435
|
+
font-feature-settings: var(--font-features-sans);
|
|
378
436
|
font-weight: 550;
|
|
379
|
-
|
|
380
|
-
|
|
437
|
+
font-size: calc(1.15rem * var(--caps-heading-size-multiplier));
|
|
438
|
+
line-height: var(--caps-heading-line-height);
|
|
439
|
+
text-transform: var(--caps-transform);
|
|
440
|
+
font-variant-caps: var(--caps-caps-variant);
|
|
441
|
+
letter-spacing: var(--caps-spacing);
|
|
381
442
|
}
|
|
382
443
|
|
|
383
444
|
.long-text h4 {
|
|
384
|
-
font-family: var(--font-
|
|
385
|
-
font-
|
|
445
|
+
font-family: var(--font-sans);
|
|
446
|
+
font-feature-settings: var(--font-features-sans);
|
|
447
|
+
font-weight: 540;
|
|
386
448
|
font-style: italic;
|
|
449
|
+
letter-spacing: 0.015em;
|
|
387
450
|
}
|
|
388
451
|
|
|
389
452
|
.long-text h5 {
|
|
@@ -407,6 +470,7 @@ hr:before {
|
|
|
407
470
|
|
|
408
471
|
.long-text .sans-text {
|
|
409
472
|
font-family: var(--font-sans);
|
|
473
|
+
font-feature-settings: var(--font-features-sans);
|
|
410
474
|
}
|
|
411
475
|
|
|
412
476
|
.long-text .sans-text p {
|
|
@@ -416,6 +480,7 @@ hr:before {
|
|
|
416
480
|
|
|
417
481
|
.long-text .sans-text h1 {
|
|
418
482
|
font-family: var(--font-sans);
|
|
483
|
+
font-feature-settings: var(--font-features-sans);
|
|
419
484
|
font-size: 1.75rem;
|
|
420
485
|
font-weight: 380;
|
|
421
486
|
margin-top: 1rem;
|
|
@@ -424,6 +489,7 @@ hr:before {
|
|
|
424
489
|
|
|
425
490
|
.long-text .sans-text h2 {
|
|
426
491
|
font-family: var(--font-sans);
|
|
492
|
+
font-feature-settings: var(--font-features-sans);
|
|
427
493
|
font-size: 1.25rem;
|
|
428
494
|
font-weight: 440;
|
|
429
495
|
margin-top: 1rem;
|
|
@@ -432,10 +498,13 @@ hr:before {
|
|
|
432
498
|
|
|
433
499
|
.long-text .sans-text h3 {
|
|
434
500
|
font-family: var(--font-sans);
|
|
435
|
-
font-
|
|
501
|
+
font-feature-settings: var(--font-features-sans);
|
|
436
502
|
font-weight: var(--font-weight-sans-bold);
|
|
437
|
-
|
|
438
|
-
|
|
503
|
+
font-size: calc(1.1rem * var(--caps-heading-size-multiplier));
|
|
504
|
+
line-height: var(--caps-heading-line-height);
|
|
505
|
+
text-transform: var(--caps-transform);
|
|
506
|
+
font-variant-caps: var(--caps-caps-variant);
|
|
507
|
+
letter-spacing: var(--caps-spacing);
|
|
439
508
|
margin-top: 1rem;
|
|
440
509
|
margin-bottom: 0.8rem;
|
|
441
510
|
}
|
|
@@ -448,6 +517,7 @@ table, th, td, tbody tr {
|
|
|
448
517
|
|
|
449
518
|
table {
|
|
450
519
|
font-family: var(--font-sans);
|
|
520
|
+
font-feature-settings: var(--font-features-sans);
|
|
451
521
|
font-size: var(--font-size-small);
|
|
452
522
|
width: auto;
|
|
453
523
|
margin-left: auto;
|
|
@@ -460,10 +530,12 @@ table {
|
|
|
460
530
|
|
|
461
531
|
th {
|
|
462
532
|
font-weight: var(--font-weight-sans-bold);
|
|
463
|
-
|
|
464
|
-
|
|
533
|
+
font-size: calc(var(--font-size-small) * var(--caps-heading-size-multiplier));
|
|
534
|
+
line-height: var(--caps-heading-line-height);
|
|
535
|
+
text-transform: var(--caps-transform);
|
|
536
|
+
font-variant-caps: var(--caps-caps-variant);
|
|
537
|
+
letter-spacing: var(--caps-spacing);
|
|
465
538
|
border-bottom: 1px solid var(--color-border-hint);
|
|
466
|
-
line-height: 1.2;
|
|
467
539
|
background-color: var(--color-bg-alt-solid);
|
|
468
540
|
}
|
|
469
541
|
|
|
@@ -527,12 +599,16 @@ sup {
|
|
|
527
599
|
|
|
528
600
|
.footnote-ref a, .footnote {
|
|
529
601
|
font-family: var(--font-sans);
|
|
602
|
+
font-feature-settings: var(--font-features-sans);
|
|
603
|
+
background-color: var(--color-bg-meta-solid);
|
|
604
|
+
color: var(--color-hint-strong);
|
|
530
605
|
text-decoration: none;
|
|
531
606
|
padding: 0 0.15rem;
|
|
532
|
-
|
|
607
|
+
margin-right: 0.15rem;
|
|
608
|
+
border-radius: 6px;
|
|
533
609
|
transition: all 0.15s ease-in-out;
|
|
534
610
|
font-style: normal;
|
|
535
|
-
font-weight:
|
|
611
|
+
font-weight: 600;
|
|
536
612
|
}
|
|
537
613
|
|
|
538
614
|
.footnote-ref a:hover, .footnote:hover {
|
|
@@ -540,8 +616,257 @@ sup {
|
|
|
540
616
|
color: var(--color-primary-light);
|
|
541
617
|
text-decoration: none;
|
|
542
618
|
}
|
|
619
|
+
|
|
620
|
+
@media print {
|
|
621
|
+
sup {
|
|
622
|
+
font-size: 70% !important;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/* Don't use stylized footnotes in print. */
|
|
626
|
+
.footnote-ref a, .footnote {
|
|
627
|
+
font-family: var(--font-serif);
|
|
628
|
+
font-feature-settings: normal !important;
|
|
629
|
+
background-color: transparent !important;
|
|
630
|
+
color: var(--color-text) !important;
|
|
631
|
+
padding: 0 0.05rem !important;
|
|
632
|
+
font-weight: 400 !important;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.footnote-ref a:hover, .footnote:hover {
|
|
636
|
+
background-color: transparent !important;
|
|
637
|
+
color: var(--color-text) !important;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/* Hide footnote return arrows/links in print */
|
|
641
|
+
.footnote-backref,
|
|
642
|
+
.footnote-return,
|
|
643
|
+
a[href^="#fnref"],
|
|
644
|
+
.reversefootnote {
|
|
645
|
+
display: none !important;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/* Also hide common up arrow characters */
|
|
649
|
+
.footnote::after[content*="↩"],
|
|
650
|
+
.footnote::after[content*="↑"] {
|
|
651
|
+
display: none !important;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
543
655
|
{% endblock footnote_styles %}
|
|
544
656
|
|
|
657
|
+
{% block print_styles %}
|
|
658
|
+
/* Print media adjustments for better readability and layout */
|
|
659
|
+
@media print {
|
|
660
|
+
@page {
|
|
661
|
+
/* Set page margins for physical page */
|
|
662
|
+
margin: 0.7in 0.95in 0.8in 0.95in;
|
|
663
|
+
|
|
664
|
+
@top-center {
|
|
665
|
+
content: "";
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
@bottom-left {
|
|
669
|
+
content: "Formatted by Kash — github.com/jlevy/kash";
|
|
670
|
+
font-family: var(--font-sans) !important;
|
|
671
|
+
font-size: var(--font-size-small);
|
|
672
|
+
color: var(--color-tertiary) !important;
|
|
673
|
+
margin: 0 0 0.2in 0 !important;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
@bottom-right {
|
|
677
|
+
content: counter(page);
|
|
678
|
+
font-family: var(--font-sans) !important;
|
|
679
|
+
font-size: var(--font-size-small);
|
|
680
|
+
color: var(--color-tertiary) !important;
|
|
681
|
+
margin: 0 0 0.2in 0 !important;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
:root {
|
|
686
|
+
/* Slightly larger fonts for print readability */
|
|
687
|
+
--font-size-normal: 1.1rem;
|
|
688
|
+
--font-size-small: 1.0rem;
|
|
689
|
+
--font-size-smaller: 0.9rem;
|
|
690
|
+
--font-size-mono: 0.9rem;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
body {
|
|
694
|
+
/* Remove body margin since @page handles it */
|
|
695
|
+
margin: 0;
|
|
696
|
+
/* Ensure good line height for print */
|
|
697
|
+
line-height: 1.6;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/* Enable hyphenation and justification for main text content only */
|
|
701
|
+
p, blockquote {
|
|
702
|
+
hyphens: auto;
|
|
703
|
+
text-align: justify;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
.long-text {
|
|
709
|
+
/* Remove shadows and borders that don't work well in print */
|
|
710
|
+
box-shadow: none !important;
|
|
711
|
+
border: none !important;
|
|
712
|
+
/* Add print-specific margins */
|
|
713
|
+
padding: 0;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/* Ensure tables don't break layout in print */
|
|
717
|
+
.table-container {
|
|
718
|
+
position: static;
|
|
719
|
+
transform: none;
|
|
720
|
+
left: auto;
|
|
721
|
+
width: 100%;
|
|
722
|
+
max-width: 100%;
|
|
723
|
+
overflow: visible;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
table {
|
|
727
|
+
width: 100%;
|
|
728
|
+
max-width: 100%;
|
|
729
|
+
font-size: 0.9rem;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/* Adjust code blocks for print */
|
|
733
|
+
pre {
|
|
734
|
+
white-space: pre-wrap;
|
|
735
|
+
word-wrap: break-word;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/* Hide interactive elements that don't work in print */
|
|
739
|
+
.code-copy-button {
|
|
740
|
+
display: none !important;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/* Page break controls */
|
|
744
|
+
h1, h2, h3, h4, h5, h6 {
|
|
745
|
+
break-after: avoid;
|
|
746
|
+
page-break-after: avoid; /* Fallback for older browsers */
|
|
747
|
+
break-inside: avoid;
|
|
748
|
+
page-break-inside: avoid; /* Fallback for older browsers */
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/* Avoid breaking these elements */
|
|
752
|
+
blockquote, pre, .code-block-wrapper, figure, .table-container {
|
|
753
|
+
break-inside: avoid;
|
|
754
|
+
page-break-inside: avoid; /* Fallback for older browsers */
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/* Control text flow */
|
|
758
|
+
p {
|
|
759
|
+
orphans: 3; /* Minimum lines at bottom of page */
|
|
760
|
+
widows: 3; /* Minimum lines at top of page */
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/* Keep list items together when reasonable */
|
|
764
|
+
li {
|
|
765
|
+
break-inside: avoid-page;
|
|
766
|
+
page-break-inside: avoid; /* Fallback for older browsers */
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/* Hide doc metadata details in print (for now) */
|
|
770
|
+
details.metadata {
|
|
771
|
+
display: none !important;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/* XXX: long endnote lists cut off the numbers. This is a workaround:
|
|
775
|
+
* Custom numbering system for ordered lists using Grid for alignment */
|
|
776
|
+
ol {
|
|
777
|
+
list-style: none;
|
|
778
|
+
counter-reset: list-counter;
|
|
779
|
+
padding-left: 0;
|
|
780
|
+
margin-left: 1rem;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
ol > li {
|
|
784
|
+
display: grid;
|
|
785
|
+
/* col 1: fixed width for numbers up to 999. col 2: for the content */
|
|
786
|
+
grid-template-columns: 2.5rem 1fr;
|
|
787
|
+
gap: 0 0.5rem; /* Space between number and content */
|
|
788
|
+
align-items: baseline; /* Aligns number with first line of text */
|
|
789
|
+
counter-increment: list-counter;
|
|
790
|
+
/* Override global li styles for grid layout */
|
|
791
|
+
margin-top: 0;
|
|
792
|
+
margin-bottom: 0.5rem;
|
|
793
|
+
position: static;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
ol > li::before {
|
|
797
|
+
content: counter(list-counter) ".";
|
|
798
|
+
text-align: right;
|
|
799
|
+
font-family: var(--font-serif);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/* Place all direct children of li into the second grid column */
|
|
803
|
+
/* This makes p, ul, etc. stack vertically as intended */
|
|
804
|
+
ol > li > * {
|
|
805
|
+
grid-column: 2;
|
|
806
|
+
word-break: break-word;
|
|
807
|
+
overflow-wrap: break-word;
|
|
808
|
+
hyphens: auto;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/* Override justification for content within list items */
|
|
812
|
+
ol > li p,
|
|
813
|
+
ol > li blockquote {
|
|
814
|
+
text-align: left !important;
|
|
815
|
+
text-justify: none !important;
|
|
816
|
+
margin-top: 0; /* Tighter spacing inside list items */
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/* --- Reset for Nested Lists --- */
|
|
820
|
+
/* This prevents nested lists from inheriting the grid layout */
|
|
821
|
+
ol ol {
|
|
822
|
+
margin-left: 1.5rem;
|
|
823
|
+
margin-top: 0.5rem;
|
|
824
|
+
list-style: decimal outside;
|
|
825
|
+
counter-reset: initial; /* Don't inherit parent counter */
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
ol ul {
|
|
829
|
+
margin-left: 1.5rem;
|
|
830
|
+
margin-top: 0.5rem;
|
|
831
|
+
list-style: none; /* We'll restore bullets with ::before */
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
ol ol > li {
|
|
835
|
+
display: list-item !important; /* Revert from grid to standard list item */
|
|
836
|
+
padding-left: 0.25rem;
|
|
837
|
+
margin-top: 0.4rem; /* Override global li margin for tighter spacing */
|
|
838
|
+
position: static; /* Override global li position */
|
|
839
|
+
/* Override justification from parent rules */
|
|
840
|
+
text-align: left !important;
|
|
841
|
+
text-justify: none !important;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
ol ul > li {
|
|
845
|
+
display: list-item !important; /* Revert from grid to standard list item */
|
|
846
|
+
padding-left: 0.25rem;
|
|
847
|
+
margin-top: 0.4rem; /* Override global li margin for tighter spacing */
|
|
848
|
+
position: relative; /* Need relative positioning for bullet ::before */
|
|
849
|
+
/* Override justification from parent rules */
|
|
850
|
+
text-align: left !important;
|
|
851
|
+
text-justify: none !important;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/* Remove the custom counter from nested ol */
|
|
855
|
+
ol ol > li::before {
|
|
856
|
+
content: "" !important;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/* Restore bullets for nested ul within ol */
|
|
860
|
+
ol ul > li::before {
|
|
861
|
+
content: "▪︎";
|
|
862
|
+
position: absolute;
|
|
863
|
+
left: -.85rem;
|
|
864
|
+
top: .25rem;
|
|
865
|
+
font-size: 0.62rem;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
{% endblock print_styles %}
|
|
869
|
+
|
|
545
870
|
{% block responsive_styles %}
|
|
546
871
|
/* Bleed wide on larger screens. */
|
|
547
872
|
/* TODO: Don't make so wide if table itself isn't large? */
|
|
@@ -152,6 +152,17 @@
|
|
|
152
152
|
src: url(https://cdn.jsdelivr.net/fontsource/fonts/pt-serif@latest/latin-700-italic.woff2) format('woff2');
|
|
153
153
|
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
|
154
154
|
}
|
|
155
|
+
/* PT Serif has a known bug with quote alignment that can look quite ugly.
|
|
156
|
+
* https://nedbatchelder.com/blog/201809/fixing_pt_serif.html?utm_source=chatgpt.com
|
|
157
|
+
* So we use a workaround to use punctuation from a different local font.
|
|
158
|
+
* After trying a few, it seems Georgia is workable for quote marks an non-oriented ASCII quotes. */
|
|
159
|
+
@font-face {
|
|
160
|
+
font-family: 'LocalPunct';
|
|
161
|
+
src: local('Georgia');
|
|
162
|
+
unicode-range:
|
|
163
|
+
U+0022, U+0027, /* " ' */
|
|
164
|
+
U+2018, U+2019, U+201C, U+201D; /* ‘ ’ “ ” */
|
|
165
|
+
}
|
|
155
166
|
/* https://fontsource.org/fonts/source-sans-3/cdn */
|
|
156
167
|
/* source-sans-3-latin-wght-normal */
|
|
157
168
|
@font-face {
|