doctracker 0.1.0__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 (38) hide show
  1. doctracker/__init__.py +5 -0
  2. doctracker/__main__.py +4 -0
  3. doctracker/cli.py +55 -0
  4. doctracker/config.py +65 -0
  5. doctracker/extraction/__init__.py +5 -0
  6. doctracker/extraction/griffe_loader.py +148 -0
  7. doctracker/server/__init__.py +1 -0
  8. doctracker/server/app.py +78 -0
  9. doctracker/server/guide.md +62 -0
  10. doctracker/server/guide.py +68 -0
  11. doctracker/server/state.py +32 -0
  12. doctracker/server/tools.py +76 -0
  13. doctracker/server/watcher.py +63 -0
  14. doctracker/server/web.py +120 -0
  15. doctracker/wiki/__init__.py +1 -0
  16. doctracker/wiki/pages.py +356 -0
  17. doctracker/wiki/static/htmx-LICENSE.txt +13 -0
  18. doctracker/wiki/static/htmx.min.js +1 -0
  19. doctracker/wiki/static/live.js +178 -0
  20. doctracker/wiki/static/style.css +424 -0
  21. doctracker/wiki/templates/base.html +33 -0
  22. doctracker/wiki/templates/entity.html +97 -0
  23. doctracker/wiki/templates/fragment.html +3 -0
  24. doctracker/wiki/templates/fragment_content.html +5 -0
  25. doctracker/wiki/templates/header.html +20 -0
  26. doctracker/wiki/templates/loading.html +2 -0
  27. doctracker/wiki/templates/members.html +18 -0
  28. doctracker/wiki/templates/module.html +19 -0
  29. doctracker/wiki/templates/not_found.html +6 -0
  30. doctracker/wiki/templates/search.html +21 -0
  31. doctracker/wiki/templates/source.html +1 -0
  32. doctracker/wiki/templates/tree.html +31 -0
  33. doctracker-0.1.0.dist-info/METADATA +234 -0
  34. doctracker-0.1.0.dist-info/RECORD +38 -0
  35. doctracker-0.1.0.dist-info/WHEEL +4 -0
  36. doctracker-0.1.0.dist-info/entry_points.txt +3 -0
  37. doctracker-0.1.0.dist-info/licenses/LICENSE +21 -0
  38. doctracker-0.1.0.dist-info/licenses/src/doctracker/wiki/static/htmx-LICENSE.txt +13 -0
@@ -0,0 +1,178 @@
1
+ // Live updates: the server sends a "changed" event when the code changes,
2
+ // and every element listening to "docs-changed" reloads itself through htmx.
3
+ const events = new EventSource("/events");
4
+ events.addEventListener("changed", () => htmx.trigger(document.body, "docs-changed"));
5
+
6
+ // Tree: remember which branches the user opened or closed, so they stay that way
7
+ // when the tree is re-rendered (navigation or live update).
8
+ const TREE_KEY = "doctracker-tree";
9
+
10
+ function loadTree() {
11
+ try {
12
+ return JSON.parse(sessionStorage.getItem(TREE_KEY)) || {};
13
+ } catch {
14
+ return {};
15
+ }
16
+ }
17
+
18
+ function saveTree(state) {
19
+ try {
20
+ sessionStorage.setItem(TREE_KEY, JSON.stringify(state));
21
+ } catch {}
22
+ }
23
+
24
+ document.addEventListener("toggle", (event) => {
25
+ const branch = event.target;
26
+ if (!branch.matches || !branch.matches("#tree details")) return;
27
+ const state = loadTree();
28
+ state[branch.dataset.path] = branch.open;
29
+ saveTree(state);
30
+ }, true);
31
+
32
+ function restoreTree() {
33
+ const state = loadTree();
34
+ for (const branch of document.querySelectorAll("#tree details")) {
35
+ const containsCurrent = branch.querySelector("a.current") !== null;
36
+ if (branch.dataset.path in state && !containsCurrent) branch.open = state[branch.dataset.path];
37
+ }
38
+ }
39
+
40
+ document.addEventListener("DOMContentLoaded", restoreTree);
41
+ document.addEventListener("htmx:afterSettle", restoreTree);
42
+
43
+ // Tree width (drag the resizer) and visibility (toggle button), kept per browser.
44
+ const WIDTH_KEY = "doctracker-tree-width";
45
+ const HIDDEN_KEY = "doctracker-tree-hidden";
46
+ const MIN_WIDTH = 160;
47
+ const MAX_WIDTH = 640;
48
+
49
+ function setTreeWidth(pixels) {
50
+ const width = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, pixels));
51
+ document.documentElement.style.setProperty("--tree-width", `${width}px`);
52
+ return width;
53
+ }
54
+
55
+ function readSetting(key) {
56
+ try {
57
+ return localStorage.getItem(key);
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ function writeSetting(key, value) {
64
+ try {
65
+ localStorage.setItem(key, value);
66
+ } catch {}
67
+ }
68
+
69
+ // Code theme: the select in the Source block swaps the stylesheet of the source code right away;
70
+ // htmx posts the choice to the server, which saves it in config.json.
71
+ document.addEventListener("change", (event) => {
72
+ if (!event.target.matches(".code-theme-select")) return;
73
+ document.getElementById("code-theme").href = `/code-theme/${event.target.value}.css`;
74
+ });
75
+
76
+ // Search box: the server returns the options; this handles opening, closing and the keyboard
77
+ // (↑/↓ move, Enter opens, Esc closes, "/" focuses the box from anywhere).
78
+ document.addEventListener("DOMContentLoaded", () => {
79
+ const box = document.getElementById("search");
80
+ const input = document.getElementById("search-input");
81
+ const results = document.getElementById("search-results");
82
+ const clear = document.getElementById("search-clear");
83
+ const options = () => [...results.querySelectorAll('[role="option"]')];
84
+
85
+ const open = () => {
86
+ box.classList.add("open");
87
+ input.setAttribute("aria-expanded", "true");
88
+ };
89
+ const close = () => {
90
+ box.classList.remove("open");
91
+ input.setAttribute("aria-expanded", "false");
92
+ };
93
+ const select = (index) => {
94
+ options().forEach((option, i) => option.setAttribute("aria-selected", String(i === index)));
95
+ const chosen = options()[index];
96
+ if (chosen) chosen.scrollIntoView({ block: "nearest" });
97
+ };
98
+ const selected = () => options().findIndex((option) => option.getAttribute("aria-selected") === "true");
99
+
100
+ input.addEventListener("input", () => { clear.hidden = !input.value; });
101
+ results.addEventListener("htmx:afterSwap", () => {
102
+ if (document.activeElement === input) open();
103
+ select(0);
104
+ });
105
+ input.addEventListener("focus", () => { if (input.value) open(); });
106
+
107
+ input.addEventListener("keydown", (event) => {
108
+ const count = options().length;
109
+ if (event.key === "ArrowDown" && count) {
110
+ event.preventDefault();
111
+ open();
112
+ select((selected() + 1) % count);
113
+ } else if (event.key === "ArrowUp" && count) {
114
+ event.preventDefault();
115
+ select((selected() - 1 + count) % count);
116
+ } else if (event.key === "Enter") {
117
+ event.preventDefault();
118
+ const chosen = options()[Math.max(selected(), 0)];
119
+ if (chosen) chosen.querySelector("a").click();
120
+ } else if (event.key === "Escape") {
121
+ close();
122
+ input.blur();
123
+ }
124
+ });
125
+
126
+ // Keep focus in the box while clicking an option, then close after navigating.
127
+ results.addEventListener("mousedown", (event) => event.preventDefault());
128
+ results.addEventListener("click", (event) => {
129
+ if (event.target.closest("a")) {
130
+ close();
131
+ input.blur();
132
+ }
133
+ });
134
+ input.addEventListener("blur", close);
135
+
136
+ clear.addEventListener("click", () => {
137
+ input.value = "";
138
+ clear.hidden = true;
139
+ results.innerHTML = "";
140
+ close();
141
+ input.focus();
142
+ });
143
+
144
+ document.addEventListener("keydown", (event) => {
145
+ const typing = ["INPUT", "TEXTAREA"].includes(document.activeElement.tagName);
146
+ if (event.key === "/" && !typing) {
147
+ event.preventDefault();
148
+ input.focus();
149
+ input.select();
150
+ }
151
+ });
152
+ });
153
+
154
+ document.addEventListener("DOMContentLoaded", () => {
155
+ const savedWidth = Number(readSetting(WIDTH_KEY));
156
+ if (savedWidth) setTreeWidth(savedWidth);
157
+ if (readSetting(HIDDEN_KEY) === "true") document.body.classList.add("tree-hidden");
158
+
159
+ document.getElementById("tree-toggle").addEventListener("click", () => {
160
+ const hidden = document.body.classList.toggle("tree-hidden");
161
+ writeSetting(HIDDEN_KEY, String(hidden));
162
+ });
163
+
164
+ document.getElementById("tree-resizer").addEventListener("pointerdown", (event) => {
165
+ event.preventDefault();
166
+ document.body.classList.add("resizing");
167
+ let width = null;
168
+ const move = (moveEvent) => { width = setTreeWidth(moveEvent.clientX); };
169
+ const stop = () => {
170
+ document.body.classList.remove("resizing");
171
+ document.removeEventListener("pointermove", move);
172
+ document.removeEventListener("pointerup", stop);
173
+ if (width) writeSetting(WIDTH_KEY, String(width));
174
+ };
175
+ document.addEventListener("pointermove", move);
176
+ document.addEventListener("pointerup", stop);
177
+ });
178
+ });
@@ -0,0 +1,424 @@
1
+ :root {
2
+ --bg: #ffffff;
3
+ --fg: #1f2328;
4
+ --muted: #656d76;
5
+ --border: #d0d7de;
6
+ --panel: #f6f8fa;
7
+ --accent: #0969da;
8
+ --class: #b35900;
9
+ --function: #8250df;
10
+ --method: #0a7d6d;
11
+ --attribute: #1a7f37;
12
+ --block: #fafbfc;
13
+ --exports: #bf3989;
14
+ --returns: #0b7a9b;
15
+ --raises: #cf222e;
16
+ --note: #9a6700;
17
+ --neutral: #6e7781;
18
+ color-scheme: light dark;
19
+ }
20
+
21
+ @media (prefers-color-scheme: dark) {
22
+ :root {
23
+ --bg: #0d1117;
24
+ --fg: #e6edf3;
25
+ --muted: #8d96a0;
26
+ --border: #30363d;
27
+ --panel: #161b22;
28
+ --accent: #4493f8;
29
+ --class: #f0a35c;
30
+ --function: #c4a1ff;
31
+ --method: #4fd1bd;
32
+ --attribute: #57c46b;
33
+ --block: #11151b;
34
+ --exports: #f778ba;
35
+ --returns: #39c5cf;
36
+ --raises: #ff7b72;
37
+ --note: #d29922;
38
+ --neutral: #8b949e;
39
+ }
40
+ }
41
+
42
+ * { box-sizing: border-box; }
43
+
44
+ :root { --topbar-height: 3.4rem; }
45
+
46
+ body {
47
+ margin: 0;
48
+ display: grid;
49
+ grid-template-columns: var(--tree-width, 18rem) 5px 1fr;
50
+ grid-template-rows: var(--topbar-height) 1fr;
51
+ grid-template-areas: "top top top" "tree resizer main";
52
+ min-height: 100vh;
53
+ background: var(--bg);
54
+ color: var(--fg);
55
+ font: 15px/1.55 system-ui, sans-serif;
56
+ }
57
+
58
+ a { color: var(--accent); text-decoration: none; }
59
+ a:hover { text-decoration: underline; }
60
+ code, pre { font-family: ui-monospace, "Cascadia Code", Menlo, monospace; font-size: 0.92em; }
61
+
62
+ /* Top bar: tree toggle, brand and search. */
63
+ #topbar {
64
+ grid-area: top;
65
+ position: sticky;
66
+ top: 0;
67
+ z-index: 20;
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 0.9rem;
71
+ padding: 0 1rem;
72
+ background: var(--bg);
73
+ border-bottom: 1px solid var(--border);
74
+ }
75
+ #topbar .brand { color: var(--fg); font-weight: 700; font-size: 1.05rem; letter-spacing: 0.02em; }
76
+ #topbar .brand:hover { text-decoration: none; color: var(--accent); }
77
+ #tree-toggle {
78
+ flex: none;
79
+ width: 2.1rem;
80
+ height: 2.1rem;
81
+ padding: 0;
82
+ border: 0;
83
+ border-radius: 50%;
84
+ background: none;
85
+ color: var(--fg);
86
+ font-size: 1.1rem;
87
+ cursor: pointer;
88
+ }
89
+ #tree-toggle:hover { background: var(--panel); }
90
+
91
+ #tree {
92
+ grid-area: tree;
93
+ position: sticky;
94
+ top: var(--topbar-height);
95
+ height: calc(100vh - var(--topbar-height));
96
+ overflow: auto;
97
+ padding: 1rem;
98
+ background: var(--panel);
99
+ border-right: 1px solid var(--border);
100
+ }
101
+
102
+ /* Tree width: drag the resizer; the toggle button hides the tree completely. */
103
+ #tree-resizer { grid-area: resizer; cursor: col-resize; background: var(--border); opacity: 0.4; }
104
+ #tree-resizer:hover, body.resizing #tree-resizer { background: var(--accent); opacity: 1; }
105
+ body.resizing { cursor: col-resize; user-select: none; }
106
+ body.tree-hidden { grid-template-columns: 0 0 1fr; }
107
+ body.tree-hidden #tree, body.tree-hidden #tree-resizer { visibility: hidden; overflow: hidden; padding: 0; border: 0; }
108
+ main { grid-area: main; }
109
+
110
+ /* Search: outlined field with a floating options list (MUI Autocomplete style). */
111
+ .search { position: relative; flex: 1; max-width: 34rem; margin-left: auto; }
112
+ .search-field {
113
+ display: flex;
114
+ align-items: center;
115
+ height: 2.4rem;
116
+ padding: 0 0.4rem 0 0.7rem;
117
+ border: 1px solid var(--border);
118
+ border-radius: 4px;
119
+ background: var(--bg);
120
+ transition: border-color 0.15s, box-shadow 0.15s;
121
+ }
122
+ .search-field:hover { border-color: var(--fg); }
123
+ .search-field:focus-within { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
124
+ .search-icon { color: var(--muted); font-size: 1.1rem; margin-right: 0.4rem; }
125
+ #search-input {
126
+ flex: 1;
127
+ min-width: 0;
128
+ border: 0;
129
+ outline: 0;
130
+ background: none;
131
+ color: var(--fg);
132
+ font: inherit;
133
+ }
134
+ #search-input::-webkit-search-cancel-button { display: none; }
135
+ #search-clear {
136
+ width: 1.7rem;
137
+ height: 1.7rem;
138
+ border: 0;
139
+ border-radius: 50%;
140
+ background: none;
141
+ color: var(--muted);
142
+ font-size: 1.2rem;
143
+ line-height: 1;
144
+ cursor: pointer;
145
+ }
146
+ #search-clear:hover { background: var(--panel); color: var(--fg); }
147
+ .search-popup {
148
+ position: absolute;
149
+ top: calc(100% + 4px);
150
+ left: 0;
151
+ right: 0;
152
+ max-height: 24rem;
153
+ overflow: auto;
154
+ border-radius: 4px;
155
+ background: var(--bg);
156
+ box-shadow: 0 5px 5px -3px rgb(0 0 0 / 20%), 0 8px 10px 1px rgb(0 0 0 / 14%), 0 3px 14px 2px rgb(0 0 0 / 12%);
157
+ }
158
+ .search-popup:empty, .search:not(.open) .search-popup { display: none; }
159
+ .search-options { list-style: none; margin: 0; padding: 0.5rem 0; }
160
+ .search-link {
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 0.5rem;
164
+ padding: 0.45rem 1rem;
165
+ color: var(--fg);
166
+ }
167
+ .search-link:hover { text-decoration: none; }
168
+ .search-options li[role="option"]:hover { background: var(--panel); }
169
+ .search-options li[aria-selected="true"] { background: color-mix(in srgb, var(--accent) 12%, var(--bg)); }
170
+ .search-text { display: flex; flex-direction: column; flex: 1; min-width: 0; }
171
+ .search-name { font-weight: 600; }
172
+ .search-name mark { background: none; color: var(--accent); font-weight: 800; }
173
+ .search-path { color: var(--muted); font-size: 0.82rem; font-family: ui-monospace, monospace; overflow-wrap: anywhere; }
174
+ .search-snippet { margin-top: 0.15rem; color: var(--fg); font-size: 0.85rem; opacity: 0.85; }
175
+ .search-snippet mark { background: color-mix(in srgb, var(--accent) 20%, var(--bg)); color: inherit; border-radius: 2px; padding: 0 1px; }
176
+ .search-kind { color: var(--muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; }
177
+ .search-empty { padding: 0.6rem 1rem; color: var(--muted); }
178
+ #tree ul { list-style: none; margin: 0; padding-left: 0; }
179
+ #tree ul ul { margin-left: 0.55rem; padding-left: 0.55rem; border-left: 1px solid var(--border); }
180
+
181
+ /* Collapsible branches: the arrow toggles, the name navigates. */
182
+ #tree summary, #tree .leaf { display: flex; align-items: center; }
183
+ #tree summary { list-style: none; }
184
+ #tree summary::-webkit-details-marker { display: none; }
185
+ #tree summary .icon { cursor: pointer; }
186
+ #tree details[open] > summary > .icon-subpackage::before { content: "📂"; }
187
+ #tree a { flex: 1; min-width: 0; display: block; padding: 0.1rem 0.3rem; border-radius: 4px; color: var(--fg); overflow-wrap: anywhere; }
188
+ #tree a.current { background: var(--accent); color: var(--bg); }
189
+ /* Each level of the tree gets its own weight and size. */
190
+ #tree a.role-package { text-transform: uppercase; font-weight: 700; letter-spacing: 0.04em; font-size: 0.95rem; }
191
+ #tree li:has(a.role-module, a.role-subpackage) { margin-top: 0.35rem; }
192
+ #tree a.role-module, #tree a.role-subpackage { font-weight: 600; }
193
+ #tree a.role-class { font-weight: 500; }
194
+ #tree a.role-function { font-size: 0.93rem; }
195
+ #tree a.role-method { font-size: 0.88rem; color: var(--muted); }
196
+ #tree a.role-method.current { color: var(--bg); }
197
+
198
+ /* Icons and symbols: the same in the tree and on the pages. */
199
+ .icon {
200
+ flex: none;
201
+ display: inline-block;
202
+ width: 1.3rem;
203
+ text-align: center;
204
+ font: 700 0.8rem ui-monospace, monospace;
205
+ }
206
+ .icon-package::before { content: "📦"; font-size: 0.85rem; }
207
+ .icon-subpackage::before { content: "📁"; font-size: 0.85rem; }
208
+ .icon-module::before { content: "📄"; font-size: 0.85rem; }
209
+ .icon-class { color: var(--class); }
210
+ .icon-function { color: var(--function); }
211
+ .icon-method { color: var(--method); }
212
+ .icon-attribute, .icon-type-alias { color: var(--attribute); }
213
+
214
+ main { padding: 1.5rem 2.5rem; max-width: 60rem; min-width: 0; }
215
+ h1 { font-size: 1.5rem; margin: 0.2rem 0; overflow-wrap: anywhere; }
216
+ h2 { font-size: 1.05rem; margin-top: 2rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; }
217
+ h2 .count {
218
+ margin-left: 0.3rem;
219
+ padding: 0 0.45rem;
220
+ border-radius: 999px;
221
+ background: var(--panel);
222
+ border: 1px solid var(--border);
223
+ color: var(--muted);
224
+ font-size: 0.75rem;
225
+ font-weight: 500;
226
+ vertical-align: middle;
227
+ }
228
+ .kind {
229
+ margin-left: 0.4rem;
230
+ padding: 0.05rem 0.5rem;
231
+ border: 1px solid var(--border);
232
+ border-radius: 999px;
233
+ font-size: 0.7rem;
234
+ text-transform: uppercase;
235
+ letter-spacing: 0.05em;
236
+ color: var(--muted);
237
+ font-weight: 500;
238
+ vertical-align: middle;
239
+ }
240
+ .crumbs, .location, .decorators, .bases { color: var(--muted); margin: 0.2rem 0; }
241
+
242
+ /* Page header: same levels as the tree. */
243
+ .page-header { padding-bottom: 0.4rem; }
244
+ .page-header .crumbs { font-size: 0.9rem; padding-bottom: 0.5rem; margin-bottom: 0.8rem; border-bottom: 1px solid var(--border); }
245
+ .page-header .sep { margin: 0 0.2rem; }
246
+ .page-header h1 { display: flex; align-items: center; flex-wrap: wrap; gap: 0.2rem; }
247
+ .page-header h1 .icon { font-size: 1.1rem; width: 1.8rem; }
248
+ .page-header .name.role-package { text-transform: uppercase; letter-spacing: 0.04em; }
249
+ .page-header .meta { display: flex; flex-direction: column; gap: 0.15rem; margin: 0.3rem 0 0; color: var(--muted); font-size: 0.9rem; }
250
+ .page-header .path { color: var(--fg); }
251
+
252
+ /* Blocks: a bordered box with its title in a badge sitting on the top border. */
253
+ .block {
254
+ position: relative;
255
+ margin-top: 1.8rem;
256
+ padding: 1.1rem 0.9rem 0.6rem;
257
+ border: 1px solid var(--border);
258
+ border-radius: 8px;
259
+ background: var(--block);
260
+ }
261
+ .block > .badge {
262
+ position: absolute;
263
+ top: 0;
264
+ left: 0.8rem;
265
+ transform: translateY(-50%);
266
+ display: inline-flex;
267
+ align-items: center;
268
+ gap: 0.35rem;
269
+ padding: 0.12rem 0.7rem;
270
+ border-radius: 999px;
271
+ background: var(--muted);
272
+ color: var(--bg);
273
+ font-size: 0.78rem;
274
+ font-weight: 700;
275
+ text-transform: uppercase;
276
+ letter-spacing: 0.05em;
277
+ }
278
+ .block > .badge code { text-transform: none; letter-spacing: 0; color: inherit; }
279
+ /* Clicking the badge collapses the block; a collapsed block is just a divider line with the badge. */
280
+ summary.badge { list-style: none; cursor: pointer; user-select: none; }
281
+ summary.badge::-webkit-details-marker { display: none; }
282
+ summary.badge:hover { filter: brightness(1.1); }
283
+ details.block:not([open]) {
284
+ padding: 0;
285
+ min-height: 0.8rem;
286
+ border-width: 1px 0 0;
287
+ border-radius: 0;
288
+ background: none;
289
+ }
290
+ /* Badge filled with the color of the kind it lists. */
291
+ .badge-class { background: var(--class); }
292
+ .badge-function { background: var(--function); }
293
+ .badge-method { background: var(--method); }
294
+ .badge-attribute, .badge-type-alias { background: var(--attribute); }
295
+ .badge-module, .badge-subpackage, .badge-description { background: var(--accent); }
296
+ .badge-exports { background: var(--exports); }
297
+ .badge-attributes { background: var(--attribute); }
298
+ .badge-returns, .badge-yields, .badge-receives { background: var(--returns); }
299
+ .badge-raises, .badge-deprecated { background: var(--raises); }
300
+ .badge-admonition, .badge-warns { background: var(--note); }
301
+ .badge-examples, .badge-source { background: var(--neutral); }
302
+
303
+ /* Block tinted with the color of its kind: light background, colored border. */
304
+ .block-class { --tint: var(--class); }
305
+ .block-function { --tint: var(--function); }
306
+ .block-method { --tint: var(--method); }
307
+ .block-attribute, .block-attributes, .block-type-alias { --tint: var(--attribute); }
308
+ .block-module, .block-subpackage, .block-description { --tint: var(--accent); }
309
+ .block-exports { --tint: var(--exports); }
310
+ .block-returns, .block-yields, .block-receives { --tint: var(--returns); }
311
+ .block-raises, .block-deprecated { --tint: var(--raises); }
312
+ .block-admonition, .block-warns { --tint: var(--note); }
313
+ .block-examples, .block-source { --tint: var(--neutral); }
314
+ .block[class*="block-"] {
315
+ background: color-mix(in srgb, var(--tint) 6%, var(--bg));
316
+ border-color: color-mix(in srgb, var(--tint) 45%, var(--bg));
317
+ }
318
+ .block[class*="block-"] .members li { border-color: color-mix(in srgb, var(--tint) 20%, var(--bg)); }
319
+ .block[class*="block-"] .member:hover { background: color-mix(in srgb, var(--tint) 12%, var(--bg)); }
320
+ .badge .count {
321
+ margin: 0;
322
+ padding: 0 0.4rem;
323
+ border: 0;
324
+ border-radius: 999px;
325
+ background: var(--bg);
326
+ color: var(--fg);
327
+ font-size: 0.7rem;
328
+ font-weight: 700;
329
+ }
330
+ .block > :last-child { margin-bottom: 0; }
331
+ .docstring > :first-child { margin-top: 0; }
332
+ .empty { color: var(--muted); font-style: italic; margin: 0; }
333
+ .chips { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 0; }
334
+ .chip { padding: 0.1rem 0.5rem; border: 1px solid var(--border); border-radius: 6px; background: var(--panel); }
335
+
336
+ /* Member rows: icon, name in bold, rest of the signature muted, summary below. */
337
+ .members { list-style: none; padding: 0; margin: 0; }
338
+ .members li { margin: 0; border-bottom: 1px solid var(--border); }
339
+ .members li:last-child { border-bottom: 0; }
340
+ .member { display: flex; gap: 0.4rem; padding: 0.55rem 0.4rem; color: var(--fg); border-radius: 6px; }
341
+ .member:hover { background: var(--panel); text-decoration: none; }
342
+ .member .icon { margin-top: 0.15rem; }
343
+ .member-body { display: flex; flex-direction: column; min-width: 0; }
344
+ .member-signature { overflow-wrap: anywhere; }
345
+ .member-signature .name { font-weight: 700; }
346
+ .member-signature .details { color: var(--muted); }
347
+ .member .summary { color: var(--muted); font-size: 0.92rem; }
348
+
349
+ pre { background: var(--panel); border: 1px solid var(--border); border-radius: 6px; padding: 0.8rem; overflow-x: auto; }
350
+ .signature { white-space: pre-wrap; }
351
+
352
+ table { border-collapse: collapse; width: 100%; }
353
+ th, td { border: 1px solid var(--border); padding: 0.35rem 0.6rem; text-align: center; vertical-align: middle; }
354
+ th { background: var(--panel); }
355
+ td p { margin: 0; }
356
+
357
+ .items { list-style: none; padding: 0; margin: 0; }
358
+ .items li { padding: 0.45rem 0; border-bottom: 1px solid color-mix(in srgb, var(--tint, var(--border)) 20%, var(--bg)); }
359
+ .items li:last-child { border-bottom: 0; }
360
+ .items li > p { margin: 0.15rem 0 0; }
361
+ .items pre { margin: 0; }
362
+ .item-head { display: block; overflow-wrap: anywhere; }
363
+ .item-head .type, .item-head .default { color: var(--muted); }
364
+ .param-name { font-weight: 700; }
365
+ .items .empty { font-size: 0.9rem; }
366
+
367
+ /* Inside a box block nothing gets a box of its own: code and chips sit flat on the block. */
368
+ .block:not(.divider) pre { margin: 0; padding: 0; background: none; border: 0; border-radius: 0; }
369
+ .block:not(.divider) .chips { gap: 0.2rem 0.9rem; }
370
+ .block:not(.divider) .chip { padding: 0; border: 0; background: none; color: var(--tint, var(--fg)); font-weight: 600; }
371
+
372
+ /* Divider blocks (entity pages): only the top line with the badge; content keeps its own boxes. */
373
+ .block.divider,
374
+ .block.divider[class*="block-"] {
375
+ margin-top: 2.8rem;
376
+ padding: 1.2rem 0 0;
377
+ border-width: 1px 0 0;
378
+ border-radius: 0;
379
+ background: none;
380
+ }
381
+ .block.divider pre { margin: 0; }
382
+ .block.divider th { background: color-mix(in srgb, var(--tint, var(--border)) 12%, var(--bg)); }
383
+ .block.divider .members li { border-color: var(--border); }
384
+ .block.divider .member { padding: 0.6rem 0.8rem; }
385
+ .block.divider .items li { padding: 0.6rem 0.8rem; border-color: var(--border); }
386
+ .block.divider .items li > p { margin: 0.3rem 0 0; }
387
+ .block.divider .items .item-head { margin-bottom: 0.1rem; }
388
+ /* Content sits indented under the divider line, narrower than it. */
389
+ .block.divider > :not(summary) { margin-left: 1.5rem; margin-right: 1.5rem; }
390
+ .block.divider > table { width: calc(100% - 3rem); }
391
+ .block .signature { white-space: pre-wrap; }
392
+ /* Signature and source code stand out: bold and slightly larger. The source colors come from the
393
+ code theme (/code-theme/<name>.css, chosen in the "Code theme" select of the Source block). */
394
+ .block .signature code,
395
+ .block-source pre code { font-weight: 600; font-size: 0.97em; }
396
+
397
+ .theme-picker {
398
+ display: flex;
399
+ justify-content: flex-end;
400
+ align-items: center;
401
+ gap: 0.5rem;
402
+ margin-bottom: 0.4rem;
403
+ color: var(--muted);
404
+ font-size: 0.82rem;
405
+ }
406
+ .theme-picker select {
407
+ padding: 0.2rem 0.4rem;
408
+ border: 1px solid var(--border);
409
+ border-radius: 4px;
410
+ background: var(--bg);
411
+ color: var(--fg);
412
+ font: inherit;
413
+ }
414
+ .block .decorators { margin: 0 0 0.5rem; }
415
+ .block .bases { margin: 0.6rem 0 0; display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
416
+
417
+
418
+ @media (max-width: 720px) {
419
+ body, body.tree-hidden { grid-template-columns: 1fr; grid-template-rows: auto auto 1fr; grid-template-areas: "top" "tree" "main"; }
420
+ #topbar .brand { display: none; }
421
+ #tree-resizer { display: none; }
422
+ #tree { position: static; height: auto; border-right: 0; border-bottom: 1px solid var(--border); }
423
+ main { padding: 1rem; }
424
+ }
@@ -0,0 +1,33 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>{{ path or "doctracker" }} · doctracker</title>
7
+ <link rel="stylesheet" href="/static/style.css">
8
+ <link rel="stylesheet" id="code-theme" href="/code-theme/{{ code_theme }}.css" data-default="{{ code_theme }}">
9
+ <script src="/static/htmx.min.js" defer></script>
10
+ <script src="/static/live.js" defer></script>
11
+ </head>
12
+ <body>
13
+ <header id="topbar">
14
+ <button id="tree-toggle" type="button" title="Show/hide the tree" aria-label="Show/hide the tree">☰</button>
15
+ <a class="brand" href="/">doctracker</a>
16
+ <div class="search" id="search">
17
+ <div class="search-field">
18
+ <span class="search-icon" aria-hidden="true">⌕</span>
19
+ <input id="search-input" type="search" name="q" placeholder="Search modules, classes, functions…"
20
+ autocomplete="off" role="combobox" aria-expanded="false" aria-controls="search-results"
21
+ hx-get="/search" hx-trigger="input changed delay:150ms, focus" hx-target="#search-results">
22
+ <button id="search-clear" type="button" aria-label="Clear" hidden>×</button>
23
+ </div>
24
+ <div id="search-results" class="search-popup"></div>
25
+ </div>
26
+ </header>
27
+ {% include "tree.html" %}
28
+ <div id="tree-resizer" title="Drag to resize"></div>
29
+ <main id="content">
30
+ {% include "fragment_content.html" %}
31
+ </main>
32
+ </body>
33
+ </html>