mkdocs-pixel-lab 0.2.1__tar.gz → 0.2.2__tar.gz

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 (91) hide show
  1. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/PKG-INFO +1 -1
  2. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/components/details.md +70 -0
  3. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/pyproject.toml +1 -1
  4. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/assets/css/style.css +36 -1
  5. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/assets/js/main.js +19 -7
  6. mkdocs_pixel_lab-0.2.2/tests/fixture_site/docs/guide/details.md +231 -0
  7. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/fixture_site/mkdocs.yml +1 -0
  8. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/test_theme.py +201 -0
  9. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/.gitignore +0 -0
  10. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/LICENSE +0 -0
  11. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/README.md +0 -0
  12. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/THIRD_PARTY_LICENSES.md +0 -0
  13. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/assets/component-preview.svg +0 -0
  14. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/components/admonitions.md +0 -0
  15. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/components/alerts.md +0 -0
  16. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/components/code-blocks.md +0 -0
  17. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/components/index.md +0 -0
  18. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/components/media.md +0 -0
  19. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/components/metadata.md +0 -0
  20. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/components/tables.md +0 -0
  21. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/components/tabs.md +0 -0
  22. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/components/text.md +0 -0
  23. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/getting-started.md +0 -0
  24. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/index.md +0 -0
  25. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/reference/compatibility.md +0 -0
  26. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/reference/configuration.md +0 -0
  27. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/reference/plugins.md +0 -0
  28. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/reference/troubleshooting.md +0 -0
  29. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/reference/upgrading.md +0 -0
  30. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/setup/analytics.md +0 -0
  31. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/setup/branding.md +0 -0
  32. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/setup/colors.md +0 -0
  33. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/setup/fonts.md +0 -0
  34. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/setup/heading-permalinks.md +0 -0
  35. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/setup/icons.md +0 -0
  36. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/setup/index.md +0 -0
  37. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/setup/navigation.md +0 -0
  38. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/setup/page-actions.md +0 -0
  39. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/setup/related-sites.md +0 -0
  40. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/setup/repository.md +0 -0
  41. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/docs/setup/search.md +0 -0
  42. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/mkdocs.yml +0 -0
  43. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/__init__.py +0 -0
  44. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/analytics.html +0 -0
  45. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/assets/css/analytics.css +0 -0
  46. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/assets/favicon.png +0 -0
  47. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/assets/fonts/ibm-plex-mono-400.woff2 +0 -0
  48. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/assets/fonts/ibm-plex-mono-600.woff2 +0 -0
  49. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/assets/fonts/ibm-plex-mono-700.woff2 +0 -0
  50. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/assets/fonts/inter-latin.woff2 +0 -0
  51. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/assets/icons/heading-link.svg +0 -0
  52. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/assets/icons/page-edit.svg +0 -0
  53. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/assets/icons/page-source.svg +0 -0
  54. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/assets/icons/repository.svg +0 -0
  55. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/assets/icons/search.svg +0 -0
  56. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/assets/js/analytics.js +0 -0
  57. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/base.html +0 -0
  58. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/git_lastmod.py +0 -0
  59. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/github_alerts.py +0 -0
  60. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/icons.html +0 -0
  61. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/main.html +0 -0
  62. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/metadata.py +0 -0
  63. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/mkdocs_theme.yml +0 -0
  64. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/page_source.html +0 -0
  65. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/robots.py +0 -0
  66. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/site_navigation.html +0 -0
  67. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/src/mkdocs_pixel_lab/sitemap.xml +0 -0
  68. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/fixture_site/docs/assets/alignment-wide.svg +0 -0
  69. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/fixture_site/docs/assets/pixel-lab-demo.mp4 +0 -0
  70. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/fixture_site/docs/assets/pixel-lab-preview.svg +0 -0
  71. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/fixture_site/docs/assets/site-navigation-hover.svg +0 -0
  72. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/fixture_site/docs/guide/alignment.md +0 -0
  73. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/fixture_site/docs/guide/code.md +0 -0
  74. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/fixture_site/docs/guide/media.md +0 -0
  75. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/fixture_site/docs/guide/nested.md +0 -0
  76. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/fixture_site/docs/guide/tabs.md +0 -0
  77. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/fixture_site/docs/index.md +0 -0
  78. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/snapshots/desktop.png +0 -0
  79. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/snapshots/mobile.png +0 -0
  80. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/test_analytics.py +0 -0
  81. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/test_docs_workflow.py +0 -0
  82. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/test_documentation.py +0 -0
  83. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/test_git_lastmod.py +0 -0
  84. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/test_icons.py +0 -0
  85. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/test_metadata.py +0 -0
  86. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/test_permalinks.py +0 -0
  87. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/test_privacy.py +0 -0
  88. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/test_repository.py +0 -0
  89. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/test_robots.py +0 -0
  90. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/test_site_navigation.py +0 -0
  91. {mkdocs_pixel_lab-0.2.1 → mkdocs_pixel_lab-0.2.2}/tests/test_source_links.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mkdocs-pixel-lab
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: A vibrant, accessible MkDocs documentation theme by Trained-by-Humans.
5
5
  Project-URL: Homepage, https://github.com/trained-by-humans/mkdocs-pixel-lab
6
6
  Project-URL: Documentation, https://github.com/trained-by-humans/mkdocs-pixel-lab#readme
@@ -62,6 +62,76 @@ This body supports **Markdown**.
62
62
  </details>
63
63
  ```
64
64
 
65
+ ## Layout modes
66
+
67
+ Pixel Lab chooses each expanded body's layout automatically, like
68
+ [content tabs](tabs.md#layout-modes):
69
+
70
+ - A single [code block](code-blocks.md) or [table](tables.md) fills the body **flush**.
71
+ - Prose, multiple blocks, and mixed content use a **padded** inset.
72
+ - Nested details and tabs select their layouts independently.
73
+
74
+ Flush content shares the details frame without a duplicate border or surrounding
75
+ gap. Code retains its internal padding, and wide code and tables scroll
76
+ horizontally inside the body.
77
+
78
+ ???+ note "Code without an extra inset"
79
+
80
+ ```python
81
+ print("One code block fills the expanded body")
82
+ ```
83
+
84
+ ???+ note "A table without an extra inset"
85
+
86
+ | Content | Layout |
87
+ | --- | --- |
88
+ | Single code block or table | Flush |
89
+ | Prose or mixed content | Padded |
90
+
91
+ ### Scoped layout overrides
92
+
93
+ Use `.details-padded` or `.details-flush` directly on a native `<details>`
94
+ element, or wrap selected `???` blocks using
95
+ [Scope selected instances](index.md#scope-selected-instances):
96
+
97
+ <div class="details-padded" markdown="1">
98
+
99
+ ???+ note "Always padded"
100
+
101
+ ```python
102
+ print("Keep an inset around this block")
103
+ ```
104
+
105
+ </div>
106
+
107
+ ````markdown
108
+ <div class="details-padded" markdown="1">
109
+
110
+ ???+ note "Always padded"
111
+
112
+ ```python
113
+ print("Keep an inset around this block")
114
+ ```
115
+
116
+ </div>
117
+ ````
118
+
119
+ For native HTML, place the class on the element:
120
+
121
+ ```html
122
+ <details class="details-flush" open>
123
+ <summary>Always flush</summary>
124
+ <pre><code>Keep code's internal padding.</code></pre>
125
+ </details>
126
+ ```
127
+
128
+ > [!NOTE]
129
+ >
130
+ > - For nested details, the nearest details layout class controls the layout.
131
+ > - These classes affect only details; `.tabs-padded` and `.tabs-flush` affect only tabs.
132
+ > - `.details-padded` and `.details-flush` work with or without JavaScript.
133
+ > - Automatic layout requires JavaScript, without it details stay padded.
134
+
65
135
  ## Customization
66
136
 
67
137
  These theme settings customize all collapsible blocks. Different detail types
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mkdocs-pixel-lab"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "A vibrant, accessible MkDocs documentation theme by Trained-by-Humans."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -563,12 +563,47 @@ img, video {
563
563
  padding: 8px 12px;
564
564
  text-transform: uppercase;
565
565
  }
566
- .admonition > :not(.admonition-title), details > :not(summary) {
566
+ .admonition > :not(.admonition-title) {
567
567
  margin-left: 14px;
568
568
  margin-right: 14px;
569
569
  max-width: calc(100% - 28px);
570
570
  }
571
571
  .admonition-title + p { padding-bottom: 14px; }
572
+ /* Details are padded by default. Explicit classes inherit through wrappers;
573
+ JavaScript marks automatic layouts on the details element itself. */
574
+ .details-padded,
575
+ :where(details[data-details-layout="padded"]) {
576
+ --details-padding: 14px;
577
+ --details-inner-border: 2px;
578
+ --details-table-width: fit-content;
579
+ --details-cell-border: 1px;
580
+ }
581
+ .details-flush,
582
+ :where(details[data-details-layout="flush"]) {
583
+ --details-padding: 0px;
584
+ --details-inner-border: 0px;
585
+ --details-table-width: 100%;
586
+ --details-cell-border: 0px;
587
+ }
588
+ details { padding: 0 var(--details-padding, 14px) var(--details-padding, 14px); }
589
+ details > summary { margin: 0 calc(-1 * var(--details-padding, 14px)) var(--details-padding, 14px); }
590
+ details:not([open]) { padding-bottom: 0; }
591
+ details:not([open]) > summary { margin-bottom: 0; }
592
+ details > :not(summary) { margin-left: 0; margin-right: 0; max-width: 100%; }
593
+ details > pre,
594
+ details > .highlight > pre { border-width: var(--details-inner-border, 2px); max-width: 100%; }
595
+ details[open] > summary + *,
596
+ details > summary + .highlight > pre { margin-top: 0; }
597
+ details > :last-child:not(summary),
598
+ details > .highlight:last-child > pre { margin-bottom: 0; }
599
+ details > table { display: block; overflow-x: auto; }
600
+ details > .table-shell { max-width: 100%; width: var(--details-table-width, fit-content); }
601
+ details > .table-shell > .table-scroll { border-width: var(--details-inner-border, 2px); }
602
+ details > .table-shell > .table-scroll > table { min-width: 100%; }
603
+ :is(details > table, details > .table-shell > .table-scroll > table) > :first-child > tr:first-child > * { border-top-width: var(--details-cell-border, 1px); }
604
+ :is(details > table, details > .table-shell > .table-scroll > table) > :last-child > tr:last-child > * { border-bottom-width: var(--details-cell-border, 1px); }
605
+ :is(details > table, details > .table-shell > .table-scroll > table) > * > tr > :first-child { border-left-width: var(--details-cell-border, 1px); }
606
+ :is(details > table, details > .table-shell > .table-scroll > table) > * > tr > :last-child { border-right-width: var(--details-cell-border, 1px); }
572
607
 
573
608
  .tabbed-set {
574
609
  background: var(--surface);
@@ -347,19 +347,31 @@ document.addEventListener("DOMContentLoaded", () => {
347
347
  scroll.appendChild(table);
348
348
  });
349
349
 
350
- // Classify direct panel content after tables receive their scrolling wrappers.
351
- document.querySelectorAll(".tabbed-content > .tabbed-block").forEach((panel) => {
352
- const override = panel.closest(".tabs-padded, .tabs-flush");
353
- const content = panel.firstElementChild;
350
+ // Classify direct content after tables receive their scrolling wrappers.
351
+ const contentLayout = (panel, override = null, flushClass = "tabs-flush") => {
352
+ const children = Array.from(panel.children).filter((child) => !child.matches("summary"));
353
+ const content = children[0];
354
354
  const hasText = Array.from(panel.childNodes).some(
355
355
  (node) => node.nodeType === Node.TEXT_NODE && node.textContent.trim(),
356
356
  );
357
- const singleBlock = panel.children.length === 1 && !hasText && content && (
357
+ const singleBlock = children.length === 1 && !hasText && content && (
358
358
  content.matches("pre, .table-shell") ||
359
359
  (content.matches(".highlight") && content.querySelector("pre"))
360
360
  );
361
- const flush = override ? override.classList.contains("tabs-flush") : singleBlock;
362
- panel.dataset.tabLayout = flush ? "flush" : "padded";
361
+ const flush = override ? override.classList.contains(flushClass) : singleBlock;
362
+ return flush ? "flush" : "padded";
363
+ };
364
+
365
+ document.querySelectorAll(".tabbed-content > .tabbed-block").forEach((panel) => {
366
+ panel.dataset.tabLayout = contentLayout(
367
+ panel, panel.closest(".tabs-padded, .tabs-flush"), "tabs-flush",
368
+ );
369
+ });
370
+
371
+ document.querySelectorAll(".main details").forEach((details) => {
372
+ // Explicit classes are handled by CSS, with or without JavaScript.
373
+ if (details.closest(".details-padded, .details-flush")) return;
374
+ details.dataset.detailsLayout = contentLayout(details);
363
375
  });
364
376
 
365
377
  document.querySelectorAll(".tabs-demo").forEach((group) => {
@@ -0,0 +1,231 @@
1
+ # Details layouts
2
+
3
+ ## Pymdown details
4
+
5
+ <!-- Minimal reproduction of the code-only expand at:
6
+ https://ultralytics.ml-pipes.com/tutorials/isolate_segmented_objects/ -->
7
+ <div id="pym-code" markdown="1">
8
+
9
+ ??? note "Define the IsolateObjects operator"
10
+
11
+ ```python
12
+ def isolate_objects(image, masks):
13
+ return [image * mask for mask in masks] # A long line exercises scrolling without widening the expandable frame.
14
+ ```
15
+
16
+ </div>
17
+
18
+ <div id="pym-table" markdown="1">
19
+
20
+ ???+ tip "Table, initially open"
21
+
22
+ | Payload | Result |
23
+ | --- | --- |
24
+ | `a_very_long_unbroken_payload_name_to_exercise_horizontal_scrolling_in_a_narrow_details_body` | Isolated object |
25
+
26
+ </div>
27
+
28
+ <div id="pym-prose" markdown="1">
29
+
30
+ ???+ note "Prose"
31
+
32
+ Keep a comfortable inset for explanations.
33
+
34
+ </div>
35
+
36
+ <div id="pym-mixed" markdown="1">
37
+
38
+ ???+ note "Mixed content"
39
+
40
+ Explanation before code.
41
+
42
+ ```python
43
+ print("Mixed content stays padded")
44
+ ```
45
+
46
+ </div>
47
+
48
+ <div id="pym-multiple" markdown="1">
49
+
50
+ ???+ note "Multiple blocks"
51
+
52
+ ```text
53
+ First block
54
+ ```
55
+
56
+ ```text
57
+ Second block
58
+ ```
59
+
60
+ </div>
61
+
62
+ ## Native HTML
63
+
64
+ <details id="raw-code" open>
65
+ <summary>Raw code</summary>
66
+ <!-- Comments and whitespace do not count as content blocks. -->
67
+ <pre><code>A long raw code line exercises horizontal scrolling without widening the expandable frame or losing its internal code padding.</code></pre>
68
+ </details>
69
+
70
+ <details id="raw-table">
71
+ <summary>Raw table</summary>
72
+ <table><thead><tr><th>Payload</th><th>Result</th></tr></thead>
73
+ <tbody><tr><td>a_very_long_unbroken_payload_name_to_exercise_horizontal_scrolling_in_a_narrow_details_body</td><td>Isolated object</td></tr></tbody></table>
74
+ </details>
75
+
76
+ <details id="raw-prose" open>
77
+ <summary>Raw prose</summary>
78
+ <p>Keep a comfortable inset for explanations.</p>
79
+ </details>
80
+
81
+ <details id="raw-mixed" open>
82
+ <summary>Raw mixed content</summary>
83
+ <p>Explanation before code.</p>
84
+ <pre><code>Mixed content stays padded.</code></pre>
85
+ </details>
86
+
87
+ <details id="raw-multiple" open>
88
+ <summary>Raw multiple blocks</summary>
89
+ <pre><code>First block</code></pre>
90
+ <pre><code>Second block</code></pre>
91
+ </details>
92
+
93
+ <details id="raw-text" open>
94
+ <summary>Text nodes also count</summary>
95
+ An unwrapped explanation.
96
+ <pre><code>One element with a text sibling stays padded.</code></pre>
97
+ </details>
98
+
99
+ ## Explicit overrides
100
+
101
+ <div id="pym-padded-code" class="details-padded" markdown="1">
102
+
103
+ ???+ note "Padded code"
104
+
105
+ ```python
106
+ print("Explicit inset")
107
+ ```
108
+
109
+ </div>
110
+
111
+ <div id="pym-padded-table" class="details-padded" markdown="1">
112
+
113
+ ???+ note "Padded table"
114
+
115
+ | Setting | Value |
116
+ | --- | --- |
117
+ | Inset | Padded |
118
+
119
+ </div>
120
+
121
+ <div id="pym-flush-mixed" class="details-flush" markdown="1">
122
+
123
+ ???+ note "Flush mixed content"
124
+
125
+ Explicitly remove the surrounding inset.
126
+
127
+ ```python
128
+ print("Still keep internal code padding")
129
+ ```
130
+
131
+ </div>
132
+
133
+ <details id="raw-padded-code" class="details-padded" open>
134
+ <summary>Raw padded code</summary>
135
+ <pre><code>Explicit inset</code></pre>
136
+ </details>
137
+
138
+ <details id="raw-padded-table" class="details-padded" open>
139
+ <summary>Raw padded table</summary>
140
+ <table><tr><th>Setting</th><th>Value</th></tr><tr><td>Inset</td><td>Padded</td></tr></table>
141
+ </details>
142
+
143
+ <details id="raw-flush-mixed" class="details-flush" open>
144
+ <summary>Raw flush mixed content</summary>
145
+ <p>Explicitly remove the surrounding inset.</p>
146
+ <pre><code>Still keep internal code padding</code></pre>
147
+ </details>
148
+
149
+ <div id="pym-flush-code" class="details-flush" markdown="1">
150
+
151
+ ???+ note "Flush code without JavaScript"
152
+
153
+ ```python
154
+ print("A long code line scrolls horizontally while the explicit flush layout keeps its internal code padding intact.")
155
+ ```
156
+
157
+ </div>
158
+
159
+ <div id="pym-flush-table" class="details-flush" markdown="1">
160
+
161
+ ???+ note "Flush table without JavaScript"
162
+
163
+ | Payload | Result |
164
+ | --- | --- |
165
+ | `a_very_long_unbroken_payload_name_to_exercise_horizontal_scrolling_in_a_narrow_details_body` | Isolated object |
166
+
167
+ </div>
168
+
169
+ <details id="raw-flush-code" class="details-flush" open>
170
+ <summary>Raw flush code without JavaScript</summary>
171
+ <pre><code>A long code line scrolls horizontally while the explicit flush layout keeps its internal code padding intact.</code></pre>
172
+ </details>
173
+
174
+ <details id="raw-flush-table" class="details-flush" open>
175
+ <summary>Raw flush table without JavaScript</summary>
176
+ <table><thead><tr><th>Payload</th><th>Result</th></tr></thead>
177
+ <tbody><tr><td>a_very_long_unbroken_payload_name_to_exercise_horizontal_scrolling_in_a_narrow_details_body</td><td>Isolated object</td></tr></tbody></table>
178
+ </details>
179
+
180
+ ## Nested details and tabs
181
+
182
+ <div class="details-padded" markdown="1">
183
+
184
+ <details id="nested-outer" class="details-flush" open markdown="1">
185
+ <summary>Closest override wins</summary>
186
+
187
+ <details id="nested-inner" class="details-padded" open markdown="1">
188
+ <summary>Inner details override</summary>
189
+
190
+ ```python
191
+ print("Inner details are padded")
192
+ ```
193
+
194
+ </details>
195
+
196
+ <details id="nested-inherited" open>
197
+ <summary>Inherit the closest wrapper</summary>
198
+ <pre><code>Inherit flush from the outer details</code></pre>
199
+ </details>
200
+
201
+ === "Independent tabs"
202
+
203
+ ```python
204
+ print("Tabs automatically select flush despite details overrides")
205
+ ```
206
+
207
+ </details>
208
+
209
+ </div>
210
+
211
+ <details id="automatic-outer" open>
212
+ <summary>Automatic nested details</summary>
213
+ <details id="automatic-inner" open>
214
+ <summary>Inner code</summary>
215
+ <pre><code>Only the inner details automatically become flush.</code></pre>
216
+ </details>
217
+ </details>
218
+
219
+ <div id="details-in-tabs" class="tabs-flush" markdown="1">
220
+
221
+ === "Independent details"
222
+
223
+ ???+ note "Details still choose padded"
224
+
225
+ A tab override does not change the details layout.
226
+
227
+ ```text
228
+ Mixed content stays padded.
229
+ ```
230
+
231
+ </div>
@@ -14,6 +14,7 @@ nav:
14
14
  - Overview: guide/nested.md
15
15
  - Media gallery: guide/media.md
16
16
  - Tab layouts: guide/tabs.md
17
+ - Details layouts: guide/details.md
17
18
  - Code highlighting: guide/code.md
18
19
  - Component alignment: guide/alignment.md
19
20
  plugins:
@@ -651,6 +651,207 @@ def test_tabs_keep_padded_fallback_without_javascript(browser, site_url: str) ->
651
651
  context.close()
652
652
 
653
653
 
654
+ @pytest.mark.parametrize("width", [1440, 390, 320])
655
+ def test_details_content_layouts(page, site_url: str, width: int) -> None:
656
+ page.set_viewport_size({"width": width, "height": 900})
657
+ page.goto(f"{site_url}/guide/details/")
658
+ page.evaluate("document.fonts.ready")
659
+ page.add_style_tag(content="html { scroll-behavior: auto; }")
660
+ expect(page.locator("#pym-code > details")).not_to_have_attribute("open")
661
+ expect(page.locator("#pym-table > details")).to_have_attribute("open", "open")
662
+ expect(page.locator("#raw-code")).to_have_attribute("open", "")
663
+ expect(page.locator("#raw-table")).not_to_have_attribute("open")
664
+ expect(page.locator(".details-content")).to_have_count(0)
665
+ for source in ("pym", "raw"):
666
+ for kind, layout in (("code", "flush"), ("table", "flush"), ("prose", "padded"),
667
+ ("mixed", "padded"), ("multiple", "padded"),
668
+ ("padded-code", "padded"), ("padded-table", "padded"),
669
+ ("flush-mixed", "flush"), ("flush-code", "flush"), ("flush-table", "flush")):
670
+ fixture = page.locator(f"#{source}-{kind}")
671
+ details = fixture if source == "raw" else fixture.locator(":scope > details")
672
+ summary = details.locator(":scope > summary")
673
+ if details.get_attribute("open") is None:
674
+ summary.click()
675
+ content = details.locator(":scope > :not(summary)")
676
+ expect(content.first).to_be_visible()
677
+ if "-" not in kind:
678
+ expect(details).to_have_attribute("data-details-layout", layout)
679
+ else:
680
+ expect(details).not_to_have_attribute("data-details-layout")
681
+ inset = 0 if layout == "flush" else 14
682
+ expect(details).to_have_css("padding-left", f"{inset}px")
683
+ expect(details).to_have_css("padding-bottom", f"{inset}px")
684
+ outer = details.bounding_box()
685
+ summary_bounds = summary.bounding_box()
686
+ first = content.first.bounding_box()
687
+ last = content.last.bounding_box()
688
+ assert summary_bounds["x"] == pytest.approx(outer["x"] + 2)
689
+ assert summary_bounds["width"] == pytest.approx(outer["width"] - 4)
690
+ assert first["x"] == pytest.approx(outer["x"] + 2 + inset)
691
+ assert first["y"] == pytest.approx(summary_bounds["y"] + summary_bounds["height"] + inset)
692
+ assert last["y"] + last["height"] == pytest.approx(outer["y"] + outer["height"] - 2 - inset)
693
+ for pre in details.locator("pre").all():
694
+ expect(pre).to_have_css("padding-left", "20px")
695
+ expect(pre).to_have_css("box-shadow", "none")
696
+ if layout == "flush":
697
+ expect(pre).to_have_css("border-left-width", "0px")
698
+ assert pre.bounding_box()["width"] == pytest.approx(outer["width"] - 4)
699
+ if kind in ("code", "table"):
700
+ scroll = details.locator("pre" if kind == "code" else ".table-scroll")
701
+ expect(scroll).to_have_css("border-top-width", "0px")
702
+ expect(scroll).to_have_css("border-bottom-width", "0px")
703
+ expect(scroll).to_have_css("box-shadow", "none")
704
+ assert scroll.bounding_box()["width"] == pytest.approx(outer["width"] - 4)
705
+ if width < 400:
706
+ assert scroll.evaluate("el => el.scrollWidth > el.clientWidth")
707
+ scroll.evaluate("el => el.scrollLeft = 60")
708
+ assert scroll.evaluate("el => el.scrollLeft") > 0
709
+ if kind == "table":
710
+ cells = details.locator("tr").first.locator("th")
711
+ expect(cells.first).to_have_css("border-top-width", "0px")
712
+ expect(cells.first).to_have_css("border-left-width", "0px")
713
+ expect(cells.last).to_have_css("border-right-width", "0px")
714
+ elif source == "pym":
715
+ button = details.get_by_role("button", name="Copy code to clipboard")
716
+ expect(button).to_be_visible()
717
+ summary.focus()
718
+ summary.press("Enter")
719
+ expect(details).not_to_have_attribute("open")
720
+ expect(content.first).not_to_be_visible()
721
+ summary.press("Space")
722
+ expect(details).to_have_attribute("open", "")
723
+ expect(content.first).to_be_visible()
724
+ expect(page.locator("#raw-text")).to_have_attribute("data-details-layout", "padded")
725
+ assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
726
+
727
+
728
+ @pytest.mark.parametrize("width", [1440, 320])
729
+ def test_details_nested_layouts_and_overrides(page, site_url: str, width: int) -> None:
730
+ page.set_viewport_size({"width": width, "height": 900})
731
+ page.goto(f"{site_url}/guide/details/")
732
+ page.evaluate("document.fonts.ready")
733
+ page.add_style_tag(content="html { scroll-behavior: auto; }")
734
+ for identifier, layout in (("nested-outer", "flush"), ("nested-inner", "padded"),
735
+ ("nested-inherited", "flush"), ("automatic-outer", "padded"),
736
+ ("automatic-inner", "flush")):
737
+ details = page.locator(f"#{identifier}")
738
+ if identifier.startswith("automatic-"):
739
+ expect(details).to_have_attribute("data-details-layout", layout)
740
+ else:
741
+ expect(details).not_to_have_attribute("data-details-layout")
742
+ expect(details).to_have_css("padding-left", "0px" if layout == "flush" else "14px")
743
+ if identifier not in ("nested-outer", "automatic-outer"):
744
+ expect(details).to_have_css("box-shadow", "none")
745
+ tabs = page.locator("#nested-outer .tabbed-block")
746
+ expect(tabs).to_have_attribute("data-tab-layout", "flush")
747
+ expect(tabs).to_have_css("padding-left", "0px")
748
+ expect(page.locator("#details-in-tabs .tabbed-block")).to_have_attribute("data-tab-layout", "flush")
749
+ expect(page.locator("#details-in-tabs details")).to_have_css("padding-left", "14px")
750
+ outer_summary = page.locator("#automatic-outer > summary")
751
+ outer_summary.click()
752
+ expect(page.locator("#automatic-inner > pre")).not_to_be_visible()
753
+ outer_summary.click()
754
+ expect(page.locator("#automatic-inner > pre")).to_be_visible()
755
+ # An explicit class also takes precedence over an existing automatic marker.
756
+ automatic = page.locator("#pym-code > details")
757
+ automatic.evaluate("el => el.classList.add('details-padded')")
758
+ expect(automatic).to_have_css("padding-left", "14px")
759
+ expect(automatic.locator("pre")).to_have_css("border-left-width", "2px")
760
+ automatic.evaluate("el => el.classList.remove('details-padded')")
761
+ expect(automatic).to_have_css("padding-left", "0px")
762
+ assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
763
+
764
+
765
+ @pytest.mark.parametrize("width", [1440, 320])
766
+ def test_details_keep_native_padded_fallback_without_javascript(browser, site_url: str, width: int) -> None:
767
+ context = browser.new_context(java_script_enabled=False, viewport={"width": width, "height": 900})
768
+ try:
769
+ page = context.new_page()
770
+ page.goto(f"{site_url}/guide/details/")
771
+ page.evaluate("document.documentElement.style.scrollBehavior = 'auto'")
772
+ for selector in ("#pym-code > details", "#raw-code", "#pym-table > details", "#raw-table",
773
+ "#pym-padded-code > details", "#raw-padded-code"):
774
+ details = page.locator(selector)
775
+ summary = details.locator(":scope > summary")
776
+ if details.get_attribute("open") is None:
777
+ summary.click()
778
+ content = details.locator(":scope > :not(summary)").first
779
+ expect(content).to_be_visible()
780
+ assert content.bounding_box()["x"] == pytest.approx(details.bounding_box()["x"] + 16)
781
+ assert content.bounding_box()["y"] > summary.bounding_box()["y"] + summary.bounding_box()["height"]
782
+ if width == 320 and selector in ("#pym-code > details", "#raw-code", "#raw-table"):
783
+ scroll = details.locator("pre" if "code" in selector else "table")
784
+ assert scroll.evaluate("el => el.scrollWidth > el.clientWidth")
785
+ scroll.evaluate("el => el.scrollLeft = 60")
786
+ assert scroll.evaluate("el => el.scrollLeft") > 0
787
+ summary.focus()
788
+ summary.press("Enter")
789
+ expect(content).not_to_be_visible()
790
+ summary.press("Space")
791
+ expect(content).to_be_visible()
792
+ assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
793
+ finally:
794
+ context.close()
795
+
796
+
797
+ @pytest.mark.parametrize("width", [1440, 320])
798
+ def test_details_explicit_layouts_without_javascript(browser, site_url: str, width: int) -> None:
799
+ context = browser.new_context(java_script_enabled=False, viewport={"width": width, "height": 900})
800
+ try:
801
+ page = context.new_page()
802
+ page.goto(f"{site_url}/guide/details/")
803
+ page.evaluate("document.documentElement.style.scrollBehavior = 'auto'")
804
+ for source in ("pym", "raw"):
805
+ for kind, inset in (("padded-code", 14), ("padded-table", 14),
806
+ ("flush-code", 0), ("flush-table", 0), ("flush-mixed", 0)):
807
+ fixture = page.locator(f"#{source}-{kind}")
808
+ details = fixture if source == "raw" else fixture.locator(":scope > details")
809
+ content = details.locator(":scope > :not(summary)").first
810
+ bounds = details.bounding_box()
811
+ assert content.bounding_box()["x"] == pytest.approx(bounds["x"] + 2 + inset)
812
+ summary = details.locator(":scope > summary")
813
+ summary_bounds = summary.bounding_box()
814
+ assert content.bounding_box()["y"] == pytest.approx(
815
+ summary_bounds["y"] + summary_bounds["height"] + inset,
816
+ )
817
+ last = details.locator(":scope > :not(summary)").last
818
+ last_bounds = last.bounding_box()
819
+ assert last_bounds["y"] + last_bounds["height"] == pytest.approx(
820
+ bounds["y"] + bounds["height"] - 2 - inset,
821
+ )
822
+ for pre in details.locator("pre").all():
823
+ expect(pre).to_have_css("padding-left", "20px")
824
+ expect(pre).to_have_css("border-top-width", "2px" if inset else "0px")
825
+ expect(pre).to_have_css("box-shadow", "none")
826
+ if kind in ("flush-code", "flush-table"):
827
+ scroll = details.locator("pre" if kind == "flush-code" else "table")
828
+ assert scroll.bounding_box()["width"] == pytest.approx(bounds["width"] - 4)
829
+ if width == 320:
830
+ assert scroll.evaluate("el => el.scrollWidth > el.clientWidth")
831
+ scroll.evaluate("el => el.scrollLeft = 60")
832
+ assert scroll.evaluate("el => el.scrollLeft") > 0
833
+ if kind == "flush-table":
834
+ cells = details.locator("tr").first.locator("th")
835
+ expect(cells.first).to_have_css("border-top-width", "0px")
836
+ expect(cells.first).to_have_css("border-left-width", "0px")
837
+ expect(cells.last).to_have_css("border-right-width", "0px")
838
+ summary = details.locator(":scope > summary")
839
+ summary.focus()
840
+ summary.press("Enter")
841
+ expect(content).not_to_be_visible()
842
+ summary.press("Space")
843
+ expect(content).to_be_visible()
844
+ for identifier, inset in (("nested-outer", 0), ("nested-inner", 14), ("nested-inherited", 0)):
845
+ details = page.locator(f"#{identifier}")
846
+ child = details.locator(":scope > :not(summary)").first
847
+ assert child.bounding_box()["x"] == pytest.approx(details.bounding_box()["x"] + 2 + inset)
848
+ # Details overrides do not change the no-JavaScript padded tab fallback.
849
+ expect(page.locator("#nested-outer .tabbed-block")).to_have_css("padding-left", "20px")
850
+ assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
851
+ finally:
852
+ context.close()
853
+
854
+
654
855
  def test_trademark_glyph_uses_theme_colors(page, site_url: str) -> None:
655
856
  page.goto(site_url)
656
857
  badge = page.locator(".brand-trademark")