kash-shell 0.3.15__py3-none-any.whl → 0.3.17__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/render_as_html.py +3 -1
- kash/actions/core/tabbed_webpage_generate.py +3 -1
- kash/commands/base/files_command.py +2 -2
- kash/config/colors.py +97 -15
- kash/file_storage/file_store.py +71 -53
- kash/file_storage/item_file_format.py +0 -2
- kash/local_server/local_server_routes.py +1 -1
- kash/model/actions_model.py +5 -1
- kash/model/items_model.py +39 -38
- kash/shell/completions/shell_completions.py +1 -1
- kash/utils/file_utils/file_formats_model.py +6 -4
- kash/web_gen/simple_webpage.py +4 -1
- kash/web_gen/tabbed_webpage.py +8 -3
- kash/web_gen/templates/base_styles.css.jinja +110 -14
- kash/web_gen/templates/base_webpage.html.jinja +155 -1
- kash/web_gen/templates/item_view.html.jinja +4 -2
- kash/web_gen/templates/simple_webpage.html.jinja +544 -16
- kash/web_gen/templates/tabbed_webpage.html.jinja +4 -2
- kash/xontrib/kash_extension.py +3 -2
- {kash_shell-0.3.15.dist-info → kash_shell-0.3.17.dist-info}/METADATA +1 -1
- {kash_shell-0.3.15.dist-info → kash_shell-0.3.17.dist-info}/RECORD +24 -24
- {kash_shell-0.3.15.dist-info → kash_shell-0.3.17.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.15.dist-info → kash_shell-0.3.17.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.15.dist-info → kash_shell-0.3.17.dist-info}/licenses/LICENSE +0 -0
kash/model/items_model.py
CHANGED
|
@@ -219,6 +219,12 @@ class Item:
|
|
|
219
219
|
a text document, PDF or other resource, URL, etc.
|
|
220
220
|
"""
|
|
221
221
|
|
|
222
|
+
# TODO: A few cleanups:
|
|
223
|
+
# - Consider adding aliases and tags. See also Obsidian frontmatter format:
|
|
224
|
+
# https://help.obsidian.md/Editing+and+formatting/Properties#Default%20properties
|
|
225
|
+
# - Can eliminate context here as we now have ExectContext in a contextvar.
|
|
226
|
+
# - Change store_path and external_path to a StorePath and Path instead of a str.
|
|
227
|
+
|
|
222
228
|
type: ItemType
|
|
223
229
|
state: State = State.draft
|
|
224
230
|
title: str | None = None
|
|
@@ -230,17 +236,15 @@ class Item:
|
|
|
230
236
|
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
231
237
|
modified_at: datetime | None = None
|
|
232
238
|
|
|
233
|
-
# TODO: Consider adding aliases and tags. See also Obsidian frontmatter format:
|
|
234
|
-
# https://help.obsidian.md/Editing+and+formatting/Properties#Default%20properties
|
|
235
|
-
|
|
236
239
|
# Content of the item.
|
|
237
240
|
# Text items are in body. Large or binary items may be stored externally.
|
|
241
|
+
# The external_path if present should always hold the current body of the content
|
|
242
|
+
# (and body will not be set). This is necessary for large or binary files.
|
|
238
243
|
body: str | None = None
|
|
239
244
|
external_path: str | None = None
|
|
240
245
|
original_filename: str | None = None
|
|
241
246
|
|
|
242
247
|
# Path to the item in the store, if it has been saved.
|
|
243
|
-
# TODO: Migrate this to StorePath.
|
|
244
248
|
store_path: str | None = None
|
|
245
249
|
|
|
246
250
|
# Optionally, relations to other items, including any time this item is derived from.
|
|
@@ -547,7 +551,7 @@ class Item:
|
|
|
547
551
|
if filename_stem and not prefer_title:
|
|
548
552
|
return slugify_snake(filename_stem)
|
|
549
553
|
else:
|
|
550
|
-
return slugify_snake(self.
|
|
554
|
+
return slugify_snake(self.pick_title(max_len=max_len, add_ops_suffix=True))
|
|
551
555
|
|
|
552
556
|
def default_filename(self) -> str:
|
|
553
557
|
"""
|
|
@@ -560,7 +564,16 @@ class Item:
|
|
|
560
564
|
full_suffix = self.get_full_suffix()
|
|
561
565
|
return join_suffix(slug, full_suffix)
|
|
562
566
|
|
|
563
|
-
def
|
|
567
|
+
def body_heading(self, allowed_tags: tuple[str, ...] = ("h1", "h2")) -> str | None:
|
|
568
|
+
"""
|
|
569
|
+
Get the first heading (by default h1 or h2) from the body text, if present.
|
|
570
|
+
"""
|
|
571
|
+
if self.format in [Format.markdown, Format.md_html]:
|
|
572
|
+
return first_heading(self.body_text(), allowed_tags=allowed_tags)
|
|
573
|
+
# TODO: Support HTML <h1> and <h2> as well.
|
|
574
|
+
return None
|
|
575
|
+
|
|
576
|
+
def pick_title(
|
|
564
577
|
self,
|
|
565
578
|
*,
|
|
566
579
|
max_len: int = 100,
|
|
@@ -619,41 +632,20 @@ class Item:
|
|
|
619
632
|
|
|
620
633
|
return final_text
|
|
621
634
|
|
|
622
|
-
def
|
|
623
|
-
"""
|
|
624
|
-
A display title for this item. Same as abbrev_title() but will fall back
|
|
625
|
-
to the filename if it is available.
|
|
626
|
-
"""
|
|
627
|
-
display_title = self.title
|
|
628
|
-
if not display_title and self.store_path:
|
|
629
|
-
display_title = Path(self.store_path).name
|
|
630
|
-
if not display_title:
|
|
631
|
-
display_title = self.abbrev_title(pull_body_heading=True)
|
|
632
|
-
return display_title
|
|
633
|
-
|
|
634
|
-
def abbrev_description(self, max_len: int = 1000) -> str:
|
|
635
|
+
def pick_description(self, max_len: int = 1000) -> str:
|
|
635
636
|
"""
|
|
636
637
|
Get or infer description.
|
|
637
638
|
"""
|
|
638
639
|
return abbrev_on_words(html_to_plaintext(self.description or self.body or ""), max_len)
|
|
639
640
|
|
|
640
|
-
def body_heading(self) -> str | None:
|
|
641
|
-
"""
|
|
642
|
-
Get the first h1 or h2 heading from the body text, if present.
|
|
643
|
-
"""
|
|
644
|
-
if self.format in [Format.markdown, Format.md_html]:
|
|
645
|
-
return first_heading(self.body_text(), allowed_tags=("h1", "h2"))
|
|
646
|
-
# TODO: Support HTML <h1> and <h2> as well.
|
|
647
|
-
return None
|
|
648
|
-
|
|
649
641
|
def abbrev_body(self, max_len: int) -> str:
|
|
650
642
|
"""
|
|
651
643
|
Get an abbreviated version of the body text. Must not be a binary Item.
|
|
652
644
|
Abbreviates YAML bodies like {"role": "user", "content": "Hello"} to "user Hello".
|
|
653
645
|
"""
|
|
654
|
-
body_text = self.body_text()
|
|
646
|
+
body_text = abbrev_str(self.body_text(), max_len)
|
|
655
647
|
|
|
656
|
-
# Just for aesthetics especially for titles of chat files.
|
|
648
|
+
# Just for aesthetics, especially for titles of chat files.
|
|
657
649
|
if self.type in [ItemType.chat, ItemType.config] or self.format == Format.yaml:
|
|
658
650
|
try:
|
|
659
651
|
yaml_obj = list(new_yaml().load_all(self.body_text()))
|
|
@@ -662,7 +654,7 @@ class Item:
|
|
|
662
654
|
except Exception as e:
|
|
663
655
|
log.info("Error parsing YAML body: %s", e)
|
|
664
656
|
|
|
665
|
-
return body_text
|
|
657
|
+
return abbrev_str(body_text, max_len)
|
|
666
658
|
|
|
667
659
|
@property
|
|
668
660
|
def has_body(self) -> bool:
|
|
@@ -745,9 +737,14 @@ class Item:
|
|
|
745
737
|
self,
|
|
746
738
|
other: Item | None = None,
|
|
747
739
|
update_timestamp: bool = False,
|
|
740
|
+
clear_fields=(
|
|
741
|
+
"store_path", # Will be set at save time.
|
|
742
|
+
"source", # Should be cleared so the ItemId of a copy is not the same as the original.
|
|
743
|
+
"modified_at",
|
|
744
|
+
),
|
|
748
745
|
**other_updates: Unpack[ItemUpdateOptions],
|
|
749
746
|
) -> dict[str, Any]:
|
|
750
|
-
overrides: dict[str, Any] = {
|
|
747
|
+
overrides: dict[str, Any] = {f: None for f in clear_fields}
|
|
751
748
|
if update_timestamp:
|
|
752
749
|
overrides["created_at"] = datetime.now()
|
|
753
750
|
|
|
@@ -767,8 +764,9 @@ class Item:
|
|
|
767
764
|
self, update_timestamp: bool = True, **other_updates: Unpack[ItemUpdateOptions]
|
|
768
765
|
) -> Item:
|
|
769
766
|
"""
|
|
770
|
-
Copy item with the given field updates. Resets `store_path` to None
|
|
771
|
-
|
|
767
|
+
Copy item with the given field updates. Resets `store_path` and `source` to None
|
|
768
|
+
since those should be set explicitly later. Preserves other fields, including
|
|
769
|
+
the body.
|
|
772
770
|
"""
|
|
773
771
|
new_fields = self._copy_and_update(update_timestamp=update_timestamp, **other_updates)
|
|
774
772
|
return Item(**new_fields)
|
|
@@ -783,9 +781,12 @@ class Item:
|
|
|
783
781
|
|
|
784
782
|
def derived_copy(self, **updates: Unpack[ItemUpdateOptions]) -> Item:
|
|
785
783
|
"""
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
784
|
+
Copy item with the given field updates. Resets `store_path` and `source` to None
|
|
785
|
+
since those should be set explicitly later. Preserves other fields, including
|
|
786
|
+
the body.
|
|
787
|
+
|
|
788
|
+
Same as `new_copy_with` but also updates the `derived_from` relation. If we also
|
|
789
|
+
have an action context, then use the `title_template` to derive a new title.
|
|
789
790
|
"""
|
|
790
791
|
if not self.store_path:
|
|
791
792
|
if self.relations.derived_from:
|
|
@@ -898,7 +899,7 @@ class Item:
|
|
|
898
899
|
elif self.external_path:
|
|
899
900
|
return fmt_loc(self.external_path)
|
|
900
901
|
else:
|
|
901
|
-
return repr(self.
|
|
902
|
+
return repr(self.pick_title())
|
|
902
903
|
|
|
903
904
|
def as_chat_history(self) -> ChatHistory:
|
|
904
905
|
if self.type != ItemType.chat:
|
|
@@ -106,7 +106,9 @@ class Format(Enum):
|
|
|
106
106
|
@property
|
|
107
107
|
def is_simple_text(self) -> bool:
|
|
108
108
|
"""
|
|
109
|
-
Is this plaintext or close to it, like Markdown?
|
|
109
|
+
Is this plaintext or close to it, like Markdown or Markdown with limited HTML?
|
|
110
|
+
"Simple text" should be a format that converts canonically to clean HTML.
|
|
111
|
+
Does not include full-page general HTML.
|
|
110
112
|
"""
|
|
111
113
|
return self in [self.plaintext, self.markdown, self.md_html]
|
|
112
114
|
|
|
@@ -174,8 +176,8 @@ class Format(Enum):
|
|
|
174
176
|
"""
|
|
175
177
|
Is this format compatible with frontmatter format metadata?
|
|
176
178
|
PDF and docx unfortunately won't work with frontmatter.
|
|
177
|
-
CSV does to some degree, depending on the tool, and this
|
|
178
|
-
|
|
179
|
+
CSV does to some degree, depending on the tool, and this can be useful so we support it.
|
|
180
|
+
Including JSON here (assuming it's JSON5) for similar reasons.
|
|
179
181
|
"""
|
|
180
182
|
return self in [
|
|
181
183
|
self.url,
|
|
@@ -189,7 +191,7 @@ class Format(Enum):
|
|
|
189
191
|
self.python,
|
|
190
192
|
self.shellscript,
|
|
191
193
|
self.xonsh,
|
|
192
|
-
self.csv,
|
|
194
|
+
self.csv, # Often but not always supported.
|
|
193
195
|
self.log,
|
|
194
196
|
]
|
|
195
197
|
|
kash/web_gen/simple_webpage.py
CHANGED
|
@@ -7,6 +7,7 @@ def simple_webpage_render(
|
|
|
7
7
|
item: Item,
|
|
8
8
|
page_template: str = "simple_webpage.html.jinja",
|
|
9
9
|
add_title_h1: bool = True,
|
|
10
|
+
show_theme_toggle: bool = False,
|
|
10
11
|
) -> str:
|
|
11
12
|
"""
|
|
12
13
|
Generate a simple web page from a single item.
|
|
@@ -15,10 +16,12 @@ def simple_webpage_render(
|
|
|
15
16
|
return render_web_template(
|
|
16
17
|
template_filename=page_template,
|
|
17
18
|
data={
|
|
18
|
-
"title": item.
|
|
19
|
+
"title": item.pick_title(),
|
|
19
20
|
"add_title_h1": add_title_h1,
|
|
20
21
|
"content_html": item.body_as_html(),
|
|
21
22
|
"thumbnail_url": item.thumbnail_url,
|
|
23
|
+
"enable_themes": show_theme_toggle,
|
|
24
|
+
"show_theme_toggle": show_theme_toggle,
|
|
22
25
|
},
|
|
23
26
|
)
|
|
24
27
|
|
kash/web_gen/tabbed_webpage.py
CHANGED
|
@@ -68,14 +68,14 @@ def tabbed_webpage_config(
|
|
|
68
68
|
|
|
69
69
|
tabs = [
|
|
70
70
|
TabInfo(
|
|
71
|
-
label=clean_label(item.
|
|
71
|
+
label=clean_label(item.pick_title()),
|
|
72
72
|
store_path=item.store_path,
|
|
73
73
|
thumbnail_url=get_thumbnail_url(item),
|
|
74
74
|
)
|
|
75
75
|
for item in items
|
|
76
76
|
]
|
|
77
77
|
_fill_in_ids(tabs)
|
|
78
|
-
title = summary_heading([item.
|
|
78
|
+
title = summary_heading([item.pick_title() for item in items])
|
|
79
79
|
config = TabbedWebpage(
|
|
80
80
|
title=title, tabs=tabs, show_tabs=len(tabs) > 1, add_title_h1=add_title_h1
|
|
81
81
|
)
|
|
@@ -100,7 +100,10 @@ def _load_tab_content(config: TabbedWebpage):
|
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
def tabbed_webpage_generate(
|
|
103
|
-
config_item: Item,
|
|
103
|
+
config_item: Item,
|
|
104
|
+
page_template: str = "base_webpage.html.jinja",
|
|
105
|
+
add_title_h1: bool = True,
|
|
106
|
+
show_theme_toggle: bool = False,
|
|
104
107
|
) -> str:
|
|
105
108
|
"""
|
|
106
109
|
Generate a web page using the supplied config.
|
|
@@ -121,6 +124,8 @@ def tabbed_webpage_generate(
|
|
|
121
124
|
"title": tabbed_webpage.title,
|
|
122
125
|
"add_title_h1": add_title_h1,
|
|
123
126
|
"content": content,
|
|
127
|
+
"enable_themes": show_theme_toggle,
|
|
128
|
+
"show_theme_toggle": show_theme_toggle,
|
|
124
129
|
},
|
|
125
130
|
)
|
|
126
131
|
|
|
@@ -176,11 +176,10 @@ ol > li {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
blockquote {
|
|
179
|
-
border-left:
|
|
179
|
+
border-left: 2px solid var(--color-primary);
|
|
180
180
|
padding-left: 1rem;
|
|
181
181
|
margin-left: 0;
|
|
182
182
|
margin-right: 0;
|
|
183
|
-
color: var(--color-secondary);
|
|
184
183
|
}
|
|
185
184
|
|
|
186
185
|
/* Inline code styling */
|
|
@@ -188,6 +187,20 @@ code {
|
|
|
188
187
|
font-family: var(--font-mono);
|
|
189
188
|
font-size: var(--font-size-mono);
|
|
190
189
|
letter-spacing: -0.025em;
|
|
190
|
+
|
|
191
|
+
transition: color 0.4s ease-in-out;
|
|
192
|
+
}
|
|
193
|
+
/* For code inside pre we style the pre tag */
|
|
194
|
+
code:not(pre code) {
|
|
195
|
+
background-color: var(--color-bg-alt);
|
|
196
|
+
border-radius: 3px;
|
|
197
|
+
border: 1px solid var(--color-hint-gentle);
|
|
198
|
+
padding: 0.25em 0.2em 0.1em 0.2em;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* Code block wrapper for positioning copy button */
|
|
202
|
+
.code-block-wrapper {
|
|
203
|
+
position: relative;
|
|
191
204
|
}
|
|
192
205
|
|
|
193
206
|
/* Code blocks (pre + code) */
|
|
@@ -195,12 +208,14 @@ pre {
|
|
|
195
208
|
font-family: var(--font-mono);
|
|
196
209
|
font-size: var(--font-size-mono);
|
|
197
210
|
letter-spacing: -0.025em;
|
|
211
|
+
|
|
198
212
|
background-color: var(--color-bg-alt);
|
|
199
|
-
border-radius:
|
|
200
|
-
border: 1px
|
|
201
|
-
padding: 0.
|
|
213
|
+
border-radius: 3px;
|
|
214
|
+
border: 1px solid var(--color-hint-gentle);
|
|
215
|
+
padding: 0.25rem 0.2rem 0.1rem 0.2rem;
|
|
202
216
|
overflow-x: auto; /* Enable horizontal scrolling */
|
|
203
|
-
|
|
217
|
+
margin: 0;
|
|
218
|
+
transition: background-color 0.4s ease-in-out, border-color 0.4s ease-in-out;
|
|
204
219
|
}
|
|
205
220
|
|
|
206
221
|
/* Reset code styling when inside pre blocks */
|
|
@@ -209,6 +224,44 @@ pre > code {
|
|
|
209
224
|
line-height: 1.5; /* Improve readability */
|
|
210
225
|
}
|
|
211
226
|
|
|
227
|
+
/* Copy button for code blocks */
|
|
228
|
+
.code-copy-button {
|
|
229
|
+
position: absolute;
|
|
230
|
+
top: 0;
|
|
231
|
+
right: 0;
|
|
232
|
+
margin: 1px;
|
|
233
|
+
background: var(--color-bg-alt-solid);
|
|
234
|
+
color: var(--color-hint);
|
|
235
|
+
border: none;
|
|
236
|
+
border-radius: 0.25rem;
|
|
237
|
+
padding: 0;
|
|
238
|
+
cursor: pointer;
|
|
239
|
+
font-size: 0.75rem;
|
|
240
|
+
z-index: 10;
|
|
241
|
+
transition: all 0.2s ease-in-out;
|
|
242
|
+
display: flex;
|
|
243
|
+
align-items: center;
|
|
244
|
+
justify-content: center;
|
|
245
|
+
width: 1.5rem;
|
|
246
|
+
height: 1.5rem;
|
|
247
|
+
opacity: 0.9;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.code-copy-button:hover {
|
|
251
|
+
background: var(--color-hover-bg);
|
|
252
|
+
color: var(--color-primary);
|
|
253
|
+
opacity: 1;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.code-copy-button.copied {
|
|
257
|
+
color: var(--color-success);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.code-copy-button svg {
|
|
261
|
+
width: 0.875rem;
|
|
262
|
+
height: 0.875rem;
|
|
263
|
+
}
|
|
264
|
+
|
|
212
265
|
hr {
|
|
213
266
|
border: none;
|
|
214
267
|
height: 1.5rem;
|
|
@@ -298,7 +351,7 @@ hr:before {
|
|
|
298
351
|
font-weight: var(--font-weight-sans-bold);
|
|
299
352
|
text-transform: uppercase;
|
|
300
353
|
letter-spacing: 0.03em;
|
|
301
|
-
margin-top:
|
|
354
|
+
margin-top: 1rem;
|
|
302
355
|
margin-bottom: 0.8rem;
|
|
303
356
|
}
|
|
304
357
|
{% endblock long_text_styles %}
|
|
@@ -339,13 +392,21 @@ tbody tr:nth-child(even) {
|
|
|
339
392
|
|
|
340
393
|
/* Container for wide tables to allow tables to break out of parent width. */
|
|
341
394
|
.table-container {
|
|
342
|
-
{# max-width: calc(100vw - 6rem); #}
|
|
343
395
|
position: relative;
|
|
344
|
-
left: 50%;
|
|
345
|
-
transform: translateX(-50%);
|
|
346
396
|
box-sizing: border-box;
|
|
347
397
|
margin: 2rem 0;
|
|
348
398
|
background-color: var(--color-bg-solid);
|
|
399
|
+
/* Default: center tables within their container */
|
|
400
|
+
left: 50%;
|
|
401
|
+
transform: translateX(-50%);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* When TOC is present, simplify table container positioning */
|
|
405
|
+
.content-with-toc.has-toc .table-container {
|
|
406
|
+
/* Within grid layout, position relative to the grid column */
|
|
407
|
+
left: 50%;
|
|
408
|
+
transform: translateX(-50%);
|
|
409
|
+
/* Let the table width be controlled by the responsive styles */
|
|
349
410
|
}
|
|
350
411
|
{% endblock table_styles %}
|
|
351
412
|
|
|
@@ -399,16 +460,50 @@ sup {
|
|
|
399
460
|
|
|
400
461
|
/* Apply shadow to long-text containers on larger screens */
|
|
401
462
|
.long-text {
|
|
463
|
+
border: 1px solid var(--color-hint-gentle);
|
|
402
464
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 -2px 6px -1px rgba(0, 0, 0, 0.07);
|
|
403
465
|
}
|
|
404
466
|
/* But remove shadow when wrapped in no-shadow class */
|
|
405
467
|
.no-shadow .long-text {
|
|
406
|
-
box-shadow: none
|
|
468
|
+
box-shadow: none;
|
|
469
|
+
border: none;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* Handle TOC layouts specially - tables should bleed within their grid column */
|
|
474
|
+
@media (min-width: 1200px) {
|
|
475
|
+
/* When TOC is present, tables should bleed within the content grid column */
|
|
476
|
+
.content-with-toc.has-toc table {
|
|
477
|
+
/* Calculate available width: full viewport minus TOC column width minus margins */
|
|
478
|
+
width: calc(100vw - var(--toc-width, 16rem) - 8rem);
|
|
479
|
+
max-width: none;
|
|
480
|
+
}
|
|
481
|
+
.content-with-toc.has-toc .table-container {
|
|
482
|
+
width: calc(100vw - var(--toc-width, 16rem) - 8rem);
|
|
483
|
+
max-width: none;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* But ensure tables don't exceed their grid column space */
|
|
487
|
+
.content-with-toc.has-toc .long-text {
|
|
488
|
+
/* The content area needs to allow tables to bleed beyond the text width */
|
|
489
|
+
overflow: visible;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* Medium screens (768px - TOC breakpoint) - no TOC, tables should bleed but not as wide */
|
|
494
|
+
@media (min-width: 768px) and (max-width: 1199px) {
|
|
495
|
+
table {
|
|
496
|
+
width: calc(100vw - 6rem);
|
|
497
|
+
max-width: calc(100vw - 6rem);
|
|
498
|
+
}
|
|
499
|
+
.table-container {
|
|
500
|
+
width: calc(100vw - 6rem);
|
|
501
|
+
max-width: calc(100vw - 6rem);
|
|
407
502
|
}
|
|
408
503
|
}
|
|
409
504
|
|
|
410
505
|
/* Make narrower screens more usable for lists and tables. */
|
|
411
|
-
@media (max-width:
|
|
506
|
+
@media (max-width: 767px) {
|
|
412
507
|
/* Prevent horizontal scrolling on the body */
|
|
413
508
|
body {
|
|
414
509
|
overflow-x: hidden;
|
|
@@ -422,7 +517,7 @@ sup {
|
|
|
422
517
|
|
|
423
518
|
/* Make table containers scrollable without affecting page layout */
|
|
424
519
|
.table-container {
|
|
425
|
-
max-width:
|
|
520
|
+
max-width: calc(100vw - 3rem);
|
|
426
521
|
overflow-x: auto;
|
|
427
522
|
transform: none;
|
|
428
523
|
left: 0;
|
|
@@ -433,9 +528,10 @@ sup {
|
|
|
433
528
|
|
|
434
529
|
table {
|
|
435
530
|
font-size: var(--font-size-smaller);
|
|
436
|
-
/* Tables can be wider than container */
|
|
531
|
+
/* Tables can be wider than container on mobile */
|
|
437
532
|
width: auto;
|
|
438
533
|
min-width: 100%;
|
|
534
|
+
max-width: none;
|
|
439
535
|
}
|
|
440
536
|
|
|
441
537
|
ul, ol {
|
|
@@ -5,10 +5,32 @@
|
|
|
5
5
|
{% block meta %}
|
|
6
6
|
<meta charset="UTF-8" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
|
+
<meta name="color-scheme" content="light dark">
|
|
8
9
|
{% endblock meta %}
|
|
9
10
|
|
|
10
11
|
{% block title %}<title>{{ title }}</title>{% endblock title %}
|
|
11
12
|
|
|
13
|
+
{% block dark_mode_script %}
|
|
14
|
+
<script>
|
|
15
|
+
// Set theme before body renders to prevent flash of unstyled content
|
|
16
|
+
function applyTheme(theme) {
|
|
17
|
+
document.documentElement.dataset.theme = theme;
|
|
18
|
+
localStorage.setItem('theme', theme);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// If theme toggle is enabled, respect stored preference or system preference.
|
|
22
|
+
// Otherwise default to light mode.
|
|
23
|
+
{% if enable_themes %}
|
|
24
|
+
const storedTheme = localStorage.getItem('theme');
|
|
25
|
+
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
26
|
+
const initialTheme = storedTheme || (systemPrefersDark ? 'dark' : 'light');
|
|
27
|
+
{% else %}
|
|
28
|
+
const initialTheme = 'light';
|
|
29
|
+
{% endif %}
|
|
30
|
+
applyTheme(initialTheme);
|
|
31
|
+
</script>
|
|
32
|
+
{% endblock dark_mode_script %}
|
|
33
|
+
|
|
12
34
|
{% block head_basic %}
|
|
13
35
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
14
36
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
@@ -35,6 +57,67 @@
|
|
|
35
57
|
{% block head_extra %}{% endblock head_extra %}
|
|
36
58
|
|
|
37
59
|
<style>
|
|
60
|
+
|
|
61
|
+
body {
|
|
62
|
+
background: var(--color-bg);
|
|
63
|
+
color: var(--color-text);
|
|
64
|
+
transition: background 0.4s ease-in-out, color 0.4s ease-in-out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.button {
|
|
68
|
+
color: var(--color-hint);
|
|
69
|
+
background: var(--color-bg);
|
|
70
|
+
border: none;
|
|
71
|
+
padding: 0;
|
|
72
|
+
border-radius: 0.3rem;
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
font-size: 1rem;
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
width: 2.5rem;
|
|
79
|
+
height: 2.5rem;
|
|
80
|
+
|
|
81
|
+
/* Separate transitions for theme vs interaction */
|
|
82
|
+
transition: background-color 0.4s ease-in-out,
|
|
83
|
+
color 0.4s ease-in-out,
|
|
84
|
+
transform 0.2s ease-in-out,
|
|
85
|
+
box-shadow 0.2s ease-in-out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.button:hover {
|
|
89
|
+
background: var(--color-hover-bg);
|
|
90
|
+
color: var(--color-primary);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.button svg {
|
|
94
|
+
width: 1rem;
|
|
95
|
+
height: 1rem;
|
|
96
|
+
transition: inherit; /* Inherit the color transition */
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* Dark mode styles for buttons */
|
|
100
|
+
[data-theme="dark"] .button {
|
|
101
|
+
color: var(--color-primary-light);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
[data-theme="dark"] .button:hover {
|
|
105
|
+
background: var(--color-hover-bg);
|
|
106
|
+
color: var(--color-bright);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Positioning class for fixed buttons */
|
|
110
|
+
.fixed-button {
|
|
111
|
+
position: fixed;
|
|
112
|
+
top: 1rem;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Specific positioning and z-index for theme toggle */
|
|
116
|
+
.theme-toggle {
|
|
117
|
+
right: 1rem;
|
|
118
|
+
z-index: 100;
|
|
119
|
+
}
|
|
120
|
+
|
|
38
121
|
{% block font_faces %}
|
|
39
122
|
/* https://fontsource.org/fonts/pt-serif/cdn */
|
|
40
123
|
/* pt-serif-latin-400-normal */
|
|
@@ -108,6 +191,14 @@
|
|
|
108
191
|
</head>
|
|
109
192
|
|
|
110
193
|
<body>
|
|
194
|
+
{% block theme_toggle %}
|
|
195
|
+
{% if show_theme_toggle %}
|
|
196
|
+
<button class="button fixed-button theme-toggle" aria-label="toggle dark mode">
|
|
197
|
+
<i data-feather="moon"></i>
|
|
198
|
+
</button>
|
|
199
|
+
{% endif %}
|
|
200
|
+
{% endblock theme_toggle %}
|
|
201
|
+
|
|
111
202
|
{% block body_header %}{% endblock body_header %}
|
|
112
203
|
|
|
113
204
|
{% block main_content %}
|
|
@@ -119,6 +210,63 @@
|
|
|
119
210
|
{% block scripts %}
|
|
120
211
|
<script>
|
|
121
212
|
document.addEventListener('DOMContentLoaded', () => {
|
|
213
|
+
// Add copy buttons to code blocks
|
|
214
|
+
document.querySelectorAll('pre').forEach(pre => {
|
|
215
|
+
// Skip if already has a copy button (check parent for wrapper)
|
|
216
|
+
if (pre.parentElement.classList.contains('code-block-wrapper')) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Create wrapper div
|
|
221
|
+
const wrapper = document.createElement('div');
|
|
222
|
+
wrapper.className = 'code-block-wrapper';
|
|
223
|
+
|
|
224
|
+
// Insert wrapper before pre and move pre inside it
|
|
225
|
+
pre.parentNode.insertBefore(wrapper, pre);
|
|
226
|
+
wrapper.appendChild(pre);
|
|
227
|
+
|
|
228
|
+
// Create copy button
|
|
229
|
+
const copyButton = document.createElement('button');
|
|
230
|
+
copyButton.className = 'code-copy-button';
|
|
231
|
+
copyButton.setAttribute('aria-label', 'Copy code');
|
|
232
|
+
|
|
233
|
+
const copyIcon = typeof feather !== 'undefined' ? feather.icons.copy.toSvg() : '<i data-feather="copy"></i>';
|
|
234
|
+
const checkIcon = typeof feather !== 'undefined' ? feather.icons.check.toSvg() : '<i data-feather="check"></i>';
|
|
235
|
+
|
|
236
|
+
copyButton.innerHTML = copyIcon;
|
|
237
|
+
copyButton.addEventListener('click', async () => {
|
|
238
|
+
const codeElement = pre.querySelector('code') || pre;
|
|
239
|
+
const textToCopy = (codeElement.textContent || codeElement.innerText).trim();
|
|
240
|
+
|
|
241
|
+
// Works on modern browsers.
|
|
242
|
+
navigator.clipboard.writeText(textToCopy).then(() => {
|
|
243
|
+
copyButton.innerHTML = checkIcon;
|
|
244
|
+
copyButton.classList.add('copied');
|
|
245
|
+
|
|
246
|
+
// Reset after 2 seconds
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
copyButton.innerHTML = copyIcon;
|
|
249
|
+
copyButton.classList.remove('copied');
|
|
250
|
+
}, 2000);
|
|
251
|
+
}).catch(err => {
|
|
252
|
+
console.error('Failed to copy text: ', err);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Add button to wrapper, not to pre
|
|
257
|
+
wrapper.appendChild(copyButton);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Theme toggle (if present on page)
|
|
261
|
+
const themeToggleButton = document.querySelector('.theme-toggle');
|
|
262
|
+
if (themeToggleButton) {
|
|
263
|
+
themeToggleButton.addEventListener('click', () => {
|
|
264
|
+
const currentTheme = document.documentElement.dataset.theme;
|
|
265
|
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
266
|
+
applyTheme(newTheme);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
122
270
|
// Send messages to the parent window, in case we are in a viewport where that matters
|
|
123
271
|
// (e.g. an iframe tooltip).
|
|
124
272
|
// Request a resize of the parent viewport. This iframe size message format isn't
|
|
@@ -159,6 +307,11 @@
|
|
|
159
307
|
}
|
|
160
308
|
});
|
|
161
309
|
});
|
|
310
|
+
|
|
311
|
+
// Initialize Feather icons once at the end, after all DOM manipulation.
|
|
312
|
+
if (typeof feather !== 'undefined') {
|
|
313
|
+
feather.replace();
|
|
314
|
+
}
|
|
162
315
|
});
|
|
163
316
|
|
|
164
317
|
// Double-click to expand (e.g. expand tooltip to popover).
|
|
@@ -179,8 +332,9 @@
|
|
|
179
332
|
}
|
|
180
333
|
});
|
|
181
334
|
|
|
182
|
-
|
|
335
|
+
|
|
183
336
|
</script>
|
|
337
|
+
{% block scripts_extra %}{% endblock scripts_extra %}
|
|
184
338
|
{% endblock scripts %}
|
|
185
339
|
|
|
186
340
|
{% block analytics %}{% endblock analytics %}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
.item-header {
|
|
13
|
-
padding: 0
|
|
13
|
+
padding: 0 1rem 0rem 1rem;
|
|
14
14
|
border-bottom: 1px solid var(--color-border-hint);
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -109,7 +109,7 @@ h1.item-title {
|
|
|
109
109
|
|
|
110
110
|
.item-file-info {
|
|
111
111
|
{# font-family: var(--font-sans); #}
|
|
112
|
-
padding: 0
|
|
112
|
+
padding: 0;
|
|
113
113
|
{# font-size: var(--font-size-mono-small); #}
|
|
114
114
|
word-break: break-word;
|
|
115
115
|
}
|
|
@@ -118,6 +118,8 @@ h1.item-title {
|
|
|
118
118
|
font-family: var(--font-sans);
|
|
119
119
|
font-size: var(--font-size-small);
|
|
120
120
|
letter-spacing: 0;
|
|
121
|
+
border: none;
|
|
122
|
+
background-color: transparent;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
.item-url {
|