sphinx-filter-tabs 0.7.0__py3-none-any.whl → 0.8.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.
- filter_tabs/extension.py +51 -41
- filter_tabs/static/filter_tabs.css +28 -16
- {sphinx_filter_tabs-0.7.0.dist-info → sphinx_filter_tabs-0.8.0.dist-info}/METADATA +17 -1
- sphinx_filter_tabs-0.8.0.dist-info/RECORD +9 -0
- sphinx_filter_tabs-0.7.0.dist-info/RECORD +0 -9
- {sphinx_filter_tabs-0.7.0.dist-info → sphinx_filter_tabs-0.8.0.dist-info}/WHEEL +0 -0
- {sphinx_filter_tabs-0.7.0.dist-info → sphinx_filter_tabs-0.8.0.dist-info}/entry_points.txt +0 -0
- {sphinx_filter_tabs-0.7.0.dist-info → sphinx_filter_tabs-0.8.0.dist-info}/licenses/LICENSE +0 -0
- {sphinx_filter_tabs-0.7.0.dist-info → sphinx_filter_tabs-0.8.0.dist-info}/top_level.txt +0 -0
filter_tabs/extension.py
CHANGED
|
@@ -85,7 +85,7 @@ class FilterTabsRenderer:
|
|
|
85
85
|
self.temp_blocks: list[nodes.Node] = temp_blocks
|
|
86
86
|
|
|
87
87
|
def render_html(self) -> list[nodes.Node]:
|
|
88
|
-
"""Constructs the complete
|
|
88
|
+
"""Constructs the complete, W3C valid, and ARIA-compliant docutils node tree."""
|
|
89
89
|
# Ensure a unique ID for each filter-tabs instance on a page.
|
|
90
90
|
if not hasattr(self.env, 'filter_tabs_counter'):
|
|
91
91
|
self.env.filter_tabs_counter = 0
|
|
@@ -104,79 +104,88 @@ class FilterTabsRenderer:
|
|
|
104
104
|
}
|
|
105
105
|
style_string = "; ".join([f"{key}: {value}" for key, value in style_vars.items()])
|
|
106
106
|
|
|
107
|
-
# If debug mode is on, log the generated ID and styles
|
|
107
|
+
# If debug mode is on, log the generated ID and styles.
|
|
108
108
|
if config.filter_tabs_debug_mode:
|
|
109
109
|
logger.info(f"[sphinx-filter-tabs] ID: {group_id}, Styles: '{style_string}'")
|
|
110
110
|
|
|
111
111
|
# Create the main container node with the inline style for theming.
|
|
112
112
|
container = ContainerNode(classes=[SFT_CONTAINER], style=style_string)
|
|
113
113
|
|
|
114
|
-
# Build the semantic structure using fieldset and a hidden legend
|
|
114
|
+
# Build the semantic structure using fieldset and a hidden legend.
|
|
115
115
|
fieldset = FieldsetNode()
|
|
116
116
|
legend = LegendNode()
|
|
117
117
|
legend += nodes.Text(f"Filter by: {', '.join(self.tab_names)}")
|
|
118
118
|
fieldset += legend
|
|
119
|
-
|
|
120
|
-
# ---
|
|
121
|
-
#
|
|
119
|
+
|
|
120
|
+
# --- CSS Generation ---
|
|
121
|
+
# This generates the dynamic CSS that handles the core filtering logic.
|
|
122
122
|
css_rules = []
|
|
123
123
|
for tab_name in self.tab_names:
|
|
124
124
|
radio_id = f"{group_id}-{self._css_escape(tab_name)}"
|
|
125
|
-
|
|
126
|
-
#
|
|
125
|
+
panel_id = f"{radio_id}-panel"
|
|
126
|
+
# This rule finds the tab bar that contains the checked radio button,
|
|
127
|
+
# then finds its sibling content area and shows the correct panel inside.
|
|
127
128
|
css_rules.append(
|
|
128
|
-
f".{SFT_TAB_BAR}:has(#{radio_id}:checked) ~ "
|
|
129
|
-
f".{SFT_CONTENT} > .{SFT_PANEL}[data-filter='{tab_name}'] {{ display: block; }}"
|
|
129
|
+
f".{SFT_TAB_BAR}:has(#{radio_id}:checked) ~ .sft-content > #{panel_id} {{ display: block; }}"
|
|
130
130
|
)
|
|
131
|
-
|
|
132
|
-
#
|
|
131
|
+
|
|
132
|
+
# Write the dynamic CSS to a temporary file and add it to the build.
|
|
133
133
|
css_content = ''.join(css_rules)
|
|
134
134
|
static_dir = Path(self.env.app.outdir) / '_static'
|
|
135
135
|
static_dir.mkdir(parents=True, exist_ok=True)
|
|
136
136
|
css_filename = f"dynamic-filter-tabs-{group_id}.css"
|
|
137
137
|
(static_dir / css_filename).write_text(css_content, encoding='utf-8')
|
|
138
|
-
|
|
139
|
-
# 2. Add the temporary CSS file using the backward-compatible app.add_css_file() method.
|
|
140
|
-
# This correctly places a <link> tag in the HTML <head>.
|
|
141
138
|
self.env.app.add_css_file(css_filename)
|
|
142
139
|
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
tab_bar = nodes.container(classes=[SFT_TAB_BAR])
|
|
146
|
-
|
|
140
|
+
# --- ARIA-Compliant HTML Structure ---
|
|
141
|
+
# The tab bar container now gets the role="tablist".
|
|
142
|
+
tab_bar = nodes.container(classes=[SFT_TAB_BAR], role='tablist')
|
|
143
|
+
fieldset += tab_bar
|
|
144
|
+
|
|
145
|
+
# The content area holds all the panels.
|
|
146
|
+
content_area = nodes.container(classes=[SFT_CONTENT])
|
|
147
|
+
fieldset += content_area
|
|
148
|
+
|
|
149
|
+
# Map tab names to their content blocks for easy lookup.
|
|
150
|
+
content_map = {block['filter-name']: block.children for block in self.temp_blocks}
|
|
151
|
+
|
|
152
|
+
# 1. Create all radio buttons and labels first and add them to the tab_bar.
|
|
153
|
+
for i, tab_name in enumerate(self.tab_names):
|
|
147
154
|
radio_id = f"{group_id}-{self._css_escape(tab_name)}"
|
|
148
|
-
|
|
149
|
-
|
|
155
|
+
panel_id = f"{radio_id}-panel"
|
|
156
|
+
|
|
157
|
+
# The radio button is for state management.
|
|
150
158
|
radio = RadioInputNode(type='radio', name=group_id, ids=[radio_id])
|
|
151
159
|
|
|
152
|
-
|
|
160
|
+
is_default = (self.default_tab == tab_name) or (i == 0 and not self.default_tab)
|
|
161
|
+
if is_default:
|
|
153
162
|
radio['checked'] = 'checked'
|
|
154
163
|
tab_bar += radio
|
|
155
164
|
|
|
156
|
-
# The label
|
|
157
|
-
label = LabelNode(for_id=radio_id)
|
|
165
|
+
# The label is the visible tab. It gets role="tab" and aria-controls.
|
|
166
|
+
label = LabelNode(for_id=radio_id, role='tab', **{'aria-controls': panel_id})
|
|
167
|
+
if is_default:
|
|
168
|
+
label['aria-selected'] = 'true'
|
|
158
169
|
label += nodes.Text(tab_name)
|
|
159
170
|
tab_bar += label
|
|
160
|
-
fieldset += tab_bar
|
|
161
171
|
|
|
162
|
-
# Create
|
|
163
|
-
|
|
164
|
-
# Map tab names to their content blocks for easy lookup.
|
|
165
|
-
content_map = {block['filter-name']: block.children for block in self.temp_blocks}
|
|
166
|
-
# Ensure we create panels for all declared tabs plus the "General" tab.
|
|
167
|
-
all_tab_names = self.tab_names + ["General"]
|
|
172
|
+
# 2. Create all tab panels and add them to the content_area.
|
|
173
|
+
all_tab_names = ["General"] + self.tab_names
|
|
168
174
|
for tab_name in all_tab_names:
|
|
169
|
-
panel
|
|
175
|
+
# The "General" panel does not correspond to a specific tab control.
|
|
176
|
+
if tab_name == "General":
|
|
177
|
+
panel = PanelNode(classes=[SFT_PANEL], **{'data-filter': tab_name})
|
|
178
|
+
else:
|
|
179
|
+
radio_id = f"{group_id}-{self._css_escape(tab_name)}"
|
|
180
|
+
panel_id = f"{radio_id}-panel"
|
|
181
|
+
# The panel gets role="tabpanel" and is linked back to the label.
|
|
182
|
+
panel = PanelNode(classes=[SFT_PANEL], ids=[panel_id], role='tabpanel', **{'aria-labelledby': radio_id})
|
|
183
|
+
|
|
170
184
|
if tab_name in content_map:
|
|
171
|
-
# Use deepcopy to prevent docutils node mutation bugs. Since the same content
|
|
172
|
-
# might be referenced or processed multiple times, a deep copy ensures that
|
|
173
|
-
# each panel gets a completely independent set of nodes.
|
|
174
185
|
panel.extend(copy.deepcopy(content_map[tab_name]))
|
|
175
186
|
content_area += panel
|
|
176
|
-
fieldset += content_area
|
|
177
|
-
container.children = [fieldset]
|
|
178
187
|
|
|
179
|
-
|
|
188
|
+
container.children = [fieldset]
|
|
180
189
|
return [container]
|
|
181
190
|
|
|
182
191
|
def render_fallback(self) -> list[nodes.Node]:
|
|
@@ -239,9 +248,10 @@ class FilterTabsDirective(Directive):
|
|
|
239
248
|
and delegates the final rendering to the FilterTabsRenderer.
|
|
240
249
|
"""
|
|
241
250
|
env = self.state.document.settings.env
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
251
|
+
|
|
252
|
+
# Remove comments to prevent nesting of filter-tabs directives.
|
|
253
|
+
#if hasattr(env, 'sft_context') and env.sft_context:
|
|
254
|
+
# raise self.error("Nesting `filter-tabs` is not supported.")
|
|
245
255
|
|
|
246
256
|
# Set a context flag to indicate that we are inside a filter-tabs block.
|
|
247
257
|
if not hasattr(env, 'sft_context'):
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/* Sphinx Filter Tabs -- Static Stylesheet */
|
|
2
|
-
/* This file provides the structural and base theme styles for the component.
|
|
3
|
-
The core filtering logic (showing the active panel) is handled by a small,
|
|
4
|
-
dynamically generated <style> block created by extension.py. */
|
|
2
|
+
/* This file provides the structural and base theme styles for the component. */
|
|
5
3
|
|
|
6
4
|
/* Main container for the tab component */
|
|
7
5
|
.sft-container {
|
|
@@ -18,9 +16,19 @@
|
|
|
18
16
|
/* The <legend> is visually hidden but essential for screen reader users. */
|
|
19
17
|
.sft-legend { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
20
18
|
|
|
21
|
-
/* The <input type="radio"> buttons are the functional core
|
|
19
|
+
/* The <input type="radio"> buttons are the functional core.
|
|
20
|
+
They are hidden accessibly, not with display:none, so they remain
|
|
21
|
+
focusable for keyboard users. */
|
|
22
22
|
.sft-tab-bar > input[type="radio"] {
|
|
23
|
-
|
|
23
|
+
position: absolute;
|
|
24
|
+
width: 1px;
|
|
25
|
+
height: 1px;
|
|
26
|
+
padding: 0;
|
|
27
|
+
margin: -1px;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
clip: rect(0, 0, 0, 0);
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
border: 0;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
/* The tab bar containing the clickable labels. */
|
|
@@ -40,32 +48,38 @@
|
|
|
40
48
|
color: #555;
|
|
41
49
|
font-weight: 500;
|
|
42
50
|
/* THEME: The font size is controlled by a CSS variable. */
|
|
43
|
-
font-size: var(--sft-
|
|
51
|
+
font-size: var(--sft-font-size, 1em);
|
|
44
52
|
line-height: 1.5;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
/* THEME: The active tab label is highlighted using a CSS variable for the color.
|
|
48
|
-
The adjacent sibling selector (+) is robust and efficient. */
|
|
55
|
+
/* THEME: The active tab label is highlighted using a CSS variable for the color. */
|
|
49
56
|
.sft-tab-bar > input[type="radio"]:checked + label {
|
|
50
57
|
border-bottom-color: var(--sft-tab-highlight-color, #007bff);
|
|
51
58
|
color: #000;
|
|
52
59
|
}
|
|
53
60
|
|
|
61
|
+
/* Add a clear, visible focus ring for keyboard navigation. */
|
|
62
|
+
.sft-tab-bar > input[type="radio"]:focus + label {
|
|
63
|
+
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.6);
|
|
64
|
+
border-radius: 5px;
|
|
65
|
+
outline: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
/* The container for all content panels. */
|
|
55
69
|
.sft-content { padding: 20px; }
|
|
56
70
|
|
|
57
|
-
/* By default, hide all tab-specific panels. */
|
|
58
|
-
.sft-
|
|
71
|
+
/* By default, hide all tab-specific panels. The dynamic CSS will show the active one. */
|
|
72
|
+
.sft-content > .sft-panel {
|
|
59
73
|
display: none;
|
|
60
74
|
}
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
|
|
76
|
+
/* Always show the "General" panel, as it has no radio button to control it. */
|
|
77
|
+
.sft-content > .sft-panel[data-filter="General"] {
|
|
63
78
|
display: block;
|
|
64
79
|
}
|
|
65
|
-
/* The dynamically generated <style> block handles showing the one active panel. */
|
|
66
80
|
|
|
67
81
|
|
|
68
|
-
/* Styles for collapsible sections
|
|
82
|
+
/* Styles for collapsible sections */
|
|
69
83
|
.collapsible-section {
|
|
70
84
|
border: 1px solid #e0e0e0;
|
|
71
85
|
/* THEME: The accent color is controlled by a CSS variable. */
|
|
@@ -81,11 +95,9 @@
|
|
|
81
95
|
background-color: #f9f9f9;
|
|
82
96
|
outline: none;
|
|
83
97
|
}
|
|
84
|
-
/* Hide the default disclosure triangle in browsers like Chrome/Safari. */
|
|
85
98
|
.collapsible-section summary::-webkit-details-marker {
|
|
86
99
|
display: none;
|
|
87
100
|
}
|
|
88
|
-
/* Hide the default disclosure triangle in browsers like Firefox. */
|
|
89
101
|
.collapsible-section summary {
|
|
90
102
|
list-style: none;
|
|
91
103
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sphinx-filter-tabs
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: A Sphinx extension for accessible, JS-free filterable content tabs.
|
|
5
5
|
Author-email: Aputsiak Niels Janussen <aputsiak+sphinx@gmail.com>
|
|
6
6
|
License: GNU General Public License v3.0
|
|
@@ -45,3 +45,19 @@ This extension provides `filter-tabs` and `tab` directives to create user-friend
|
|
|
45
45
|
You can install this extension using `pip`:
|
|
46
46
|
```bash
|
|
47
47
|
pip install sphinx-filter-tabs
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Development
|
|
51
|
+
|
|
52
|
+
1. You can install a local version of the Sphinx with extension using:
|
|
53
|
+
```bash
|
|
54
|
+
./scripts/setup_dev.sh # Initially cleans previous folders in _docs/build and venv.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Command to enter venv is provided.
|
|
58
|
+
|
|
59
|
+
2. Once inside virtual environment, you can use following commands:
|
|
60
|
+
```bash
|
|
61
|
+
pytest # Runs test suite on configured version of Sphinx.
|
|
62
|
+
tox # Check across multiple Sphinx versions. Manual install of tox required.
|
|
63
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
filter_tabs/__init__.py,sha256=VPpIhj4HaLeMX7ai7dZFkUm81ii2ePPGjCd9hsMjsN4,397
|
|
2
|
+
filter_tabs/extension.py,sha256=K7w7iRN72PGVvswDuULBxxxam4zvPF1ZulsAxeHlvz8,20937
|
|
3
|
+
filter_tabs/static/filter_tabs.css,sha256=gXQk2ceyjAj9p3G_k-mrcPdxq0ggIXSMuXHyUE5mPP0,3486
|
|
4
|
+
sphinx_filter_tabs-0.8.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
5
|
+
sphinx_filter_tabs-0.8.0.dist-info/METADATA,sha256=SjLifkJnouYp8U4_te9JgpPtELGgQcaaeXEXoGRSCkU,2705
|
|
6
|
+
sphinx_filter_tabs-0.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
sphinx_filter_tabs-0.8.0.dist-info/entry_points.txt,sha256=za_bQcueY8AHyq7XnnjkW9X3C-LsZjeERVQ_ds7jV1A,62
|
|
8
|
+
sphinx_filter_tabs-0.8.0.dist-info/top_level.txt,sha256=K0Iy-6EsYYdvlyXdsJT0SQg-BLDBgT5-Y8ZKy5SNAfc,12
|
|
9
|
+
sphinx_filter_tabs-0.8.0.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
filter_tabs/__init__.py,sha256=VPpIhj4HaLeMX7ai7dZFkUm81ii2ePPGjCd9hsMjsN4,397
|
|
2
|
-
filter_tabs/extension.py,sha256=tU2ZBYWRmYHyvQh8NgPWWqFidunsL16KHhoyqMfboCw,20739
|
|
3
|
-
filter_tabs/static/filter_tabs.css,sha256=btJoI-Q87e0XUQARwJUhM6RCfg5m2dvxKtfABZ2zRsc,3429
|
|
4
|
-
sphinx_filter_tabs-0.7.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
5
|
-
sphinx_filter_tabs-0.7.0.dist-info/METADATA,sha256=WJjRU9rRRzgFFKlheXL4QTa_c1vF0FWhd3bqMyiG_zA,2264
|
|
6
|
-
sphinx_filter_tabs-0.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
sphinx_filter_tabs-0.7.0.dist-info/entry_points.txt,sha256=za_bQcueY8AHyq7XnnjkW9X3C-LsZjeERVQ_ds7jV1A,62
|
|
8
|
-
sphinx_filter_tabs-0.7.0.dist-info/top_level.txt,sha256=K0Iy-6EsYYdvlyXdsJT0SQg-BLDBgT5-Y8ZKy5SNAfc,12
|
|
9
|
-
sphinx_filter_tabs-0.7.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|