Skip to content

Commit db15bff

Browse files
authored
✨ NEW: Add card-carousel directive (#11)
1 parent 10d21dc commit db15bff

File tree

11 files changed

+250
-11
lines changed

11 files changed

+250
-11
lines changed

README.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,12 @@ Use autoprefixer when compiling SASS (see <https://getbootstrap.com/docs/5.0/get
7777

7878
horizontal card (grid row inside card, picture on left)
7979

80-
horizontally scrollable cards: (see <https://stackoverflow.com/questions/35993300/horizontally-scrollable-list-of-cards-in-bootstrap>)
81-
8280
subtitle for card (see <https://material.io/components/cards#anatomy>)
8381

8482
paragraph and tab-set in grid-item
8583

8684
rtd PRs not working
8785

88-
size octicons to 1rem etc
89-
90-
empty grid item
91-
9286
[github-ci]: https://github.com/executablebooks/sphinx-design/workflows/continuous-integration/badge.svg?branch=main
9387
[github-link]: https://github.com/executablebooks/sphinx-design
9488
[codecov-badge]: https://codecov.io/gh/executablebooks/sphinx-design/branch/main/graph/badge.svg

docs/cards.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,49 @@ Content
151151
Content
152152
:::
153153

154+
(cards:carousel)=
155+
156+
## Card carousels
157+
158+
The `card-carousel` directive can be used to create a single row of fixed width, scrollable cards.
159+
The argument should be a number between 1 and 12, to denote the number of cards to display.
160+
161+
When scrolling a carousel, the scroll will snap to the start of the nearest card:
162+
163+
::::{card-carousel} 2
164+
165+
:::{card} card 1
166+
content
167+
:::
168+
:::{card} card 2
169+
Longer
170+
171+
content
172+
:::
173+
:::{card} card 3
174+
:::
175+
:::{card} card 4
176+
:::
177+
:::{card} card 5
178+
:::
179+
:::{card} card 6
180+
:::
181+
::::
182+
183+
`````{dropdown} Syntax
184+
:icon: code
185+
:color: light
186+
187+
````{tab-set-code}
188+
```{literalinclude} ./snippets/myst/card-carousel.txt
189+
:language: markdown
190+
```
191+
```{literalinclude} ./snippets/rst/card-carousel.txt
192+
:language: rst
193+
```
194+
````
195+
`````
196+
154197
(cards:options)=
155198

156199
## `card` options
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
::::{card-carousel} 2
2+
3+
:::{card} card 1
4+
content
5+
:::
6+
:::{card} card 2
7+
Longer
8+
9+
content
10+
:::
11+
:::{card} card 3
12+
:::
13+
:::{card} card 4
14+
:::
15+
:::{card} card 5
16+
:::
17+
:::{card} card 6
18+
:::
19+
::::
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.. card-carousel:: 2
2+
3+
.. card:: card 1
4+
5+
content
6+
7+
.. card:: card 2
8+
9+
Longer
10+
11+
content
12+
13+
.. card:: card 3
14+
15+
.. card:: card 4
16+
17+
.. card:: card 5
18+
19+
.. card:: card 6

docs/tabs.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ Content 2
167167

168168
::::::
169169

170+
## `tab-set` options
171+
172+
class
173+
: Additional CSS classes for the container element.
174+
170175
## `tab-item` options
171176

172177
selected

sphinx_design/cards.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,30 @@
77
from sphinx import addnodes
88
from sphinx.application import Sphinx
99
from sphinx.util.docutils import SphinxDirective
10+
from sphinx.util.logging import getLogger
1011

1112
from .shared import (
13+
WARNING_TYPE,
1214
PassthroughTextElement,
1315
create_component,
16+
is_component,
1417
make_choice,
1518
margin_option,
1619
text_align,
1720
)
1821

22+
LOGGER = getLogger(__name__)
23+
1924
DIRECTIVE_NAME_CARD = "card"
25+
DIRECTIVE_NAME_CAROUSEL = "card-carousel"
2026
REGEX_HEADER = re.compile(r"^\^{3,}\s*$")
2127
REGEX_FOOTER = re.compile(r"^\+{3,}\s*$")
2228

2329

2430
def setup_cards(app: Sphinx) -> None:
2531
"""Setup the card components."""
2632
app.add_directive(DIRECTIVE_NAME_CARD, CardDirective)
33+
app.add_directive(DIRECTIVE_NAME_CAROUSEL, CardCarouselDirective)
2734

2835

2936
class CardContent(NamedTuple):
@@ -61,7 +68,6 @@ class CardDirective(SphinxDirective):
6168
}
6269

6370
def run(self) -> List[nodes.Node]:
64-
self.assert_has_content()
6571
return [self.create_card(self, self.arguments, self.options)]
6672

6773
@classmethod
@@ -214,3 +220,42 @@ def add_card_child_classes(node):
214220
# title["classes"] = ([] if "classes" not in title else title["classes"]) + [
215221
# "sd-card-title"
216222
# ]
223+
224+
225+
class CardCarouselDirective(SphinxDirective):
226+
"""A component, which is a container for cards in a single scrollable row."""
227+
228+
has_content = True
229+
required_arguments = 1 # columns
230+
optional_arguments = 0
231+
option_spec = {
232+
"class": directives.class_option,
233+
}
234+
235+
def run(self) -> List[nodes.Node]:
236+
"""Run the directive."""
237+
self.assert_has_content()
238+
try:
239+
cols = make_choice([str(i) for i in range(1, 13)])(
240+
self.arguments[0].strip()
241+
)
242+
except ValueError as exc:
243+
raise self.error(f"Invalid directive argument: {exc}")
244+
container = create_component(
245+
"card-carousel",
246+
["sd-sphinx-override", "sd-cards-carousel", f"sd-card-cols-{cols}"]
247+
+ self.options.get("class", []),
248+
)
249+
self.set_source_info(container)
250+
self.state.nested_parse(self.content, self.content_offset, container)
251+
for item in container.children:
252+
if not is_component(item, "card"):
253+
LOGGER.warning(
254+
"All children of a 'card-carousel' "
255+
f"should be 'card' [{WARNING_TYPE}.card]",
256+
location=item,
257+
type=WARNING_TYPE,
258+
subtype="card",
259+
)
260+
break
261+
return [container]

sphinx_design/compiled/style.min.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sphinx_design/grids.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,12 @@ class GridDirective(SphinxDirective):
113113

114114
def run(self) -> List[nodes.Node]:
115115
"""Run the directive."""
116-
column_classes = row_columns_option(self.arguments[0]) if self.arguments else []
116+
try:
117+
column_classes = (
118+
row_columns_option(self.arguments[0]) if self.arguments else []
119+
)
120+
except ValueError as exc:
121+
raise self.error(f"Invalid directive argument: {exc}")
117122
self.assert_has_content()
118123
# container-fluid is 100% width for all breakpoints,
119124
# rather than the fixed width of the breakpoint (like container)
@@ -170,7 +175,6 @@ class GridItemDirective(SphinxDirective):
170175

171176
def run(self) -> List[nodes.Node]:
172177
"""Run the directive."""
173-
self.assert_has_content()
174178
if not is_component(self.state_machine.node, "grid-row"):
175179
LOGGER.warning(
176180
f"The parent of a 'grid-item' should be a 'grid-row' [{WARNING_TYPE}.grid]",
@@ -223,7 +227,6 @@ class GridItemCardDirective(SphinxDirective):
223227

224228
def run(self) -> List[nodes.Node]:
225229
"""Run the directive."""
226-
self.assert_has_content()
227230
if not is_component(self.state_machine.node, "grid-row"):
228231
LOGGER.warning(
229232
f"The parent of a 'grid-item' should be a 'grid-row' [{WARNING_TYPE}.grid]",

style/_cards.scss

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Copyright 2011-2019 The Bootstrap Authors
33
// Copyright 2011-2019 Twitter, Inc.
44
// Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5+
@use 'sass:math';
56

67
.sd-card {
78
background-clip: border-box;
@@ -114,3 +115,43 @@
114115
border-bottom-left-radius: calc(0.25rem - 1px);
115116
border-bottom-right-radius: calc(0.25rem - 1px);
116117
}
118+
119+
// sd-cards-carousel is not part of bootstrap
120+
// it is intended to create a single row of scrollable cards
121+
// with a standard width for each card
122+
123+
.sd-cards-carousel {
124+
width: 100%;
125+
display: flex;
126+
flex-wrap: nowrap;
127+
-ms-flex-direction: row;
128+
flex-direction: row;
129+
overflow-x: hidden;
130+
scroll-snap-type: x mandatory;
131+
}
132+
133+
// use to always show the scroll bar
134+
.sd-cards-carousel.sd-show-scrollbar {
135+
overflow-x: auto;
136+
}
137+
138+
.sd-cards-carousel:hover, .sd-cards-carousel:focus {
139+
overflow-x: auto;
140+
}
141+
142+
.sd-cards-carousel > .sd-card {
143+
flex-shrink: 0;
144+
scroll-snap-align: start;
145+
}
146+
147+
.sd-cards-carousel > .sd-card:not(:last-child) {
148+
margin-right: 3px;
149+
}
150+
151+
@for $i from 1 through 12 {
152+
.sd-card-cols-#{$i} > .sd-card {
153+
// we use less than 100% here, so that the (i+1)th card will be slightly visible,
154+
// so the user is aware that there are more cards available
155+
width: math.div(90%, $i);
156+
}
157+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<document source="index">
2+
<section ids="heading" names="heading">
3+
<title>
4+
Heading
5+
<container classes="sd-sphinx-override sd-cards-carousel sd-card-cols-2" design_component="card-carousel" is_div="True">
6+
<container classes="sd-card sd-sphinx-override sd-mb-3 sd-shadow-sm" design_component="card" is_div="True">
7+
<container classes="sd-card-body" design_component="card-body" is_div="True">
8+
<container classes="sd-card-title sd-font-weight-bold" design_component="card-title" is_div="True">
9+
card 1
10+
<paragraph classes="sd-card-text">
11+
content
12+
<container classes="sd-card sd-sphinx-override sd-mb-3 sd-shadow-sm" design_component="card" is_div="True">
13+
<container classes="sd-card-body" design_component="card-body" is_div="True">
14+
<container classes="sd-card-title sd-font-weight-bold" design_component="card-title" is_div="True">
15+
card 2
16+
<paragraph classes="sd-card-text">
17+
Longer
18+
<paragraph classes="sd-card-text">
19+
content
20+
<container classes="sd-card sd-sphinx-override sd-mb-3 sd-shadow-sm" design_component="card" is_div="True">
21+
<container classes="sd-card-body" design_component="card-body" is_div="True">
22+
<container classes="sd-card-title sd-font-weight-bold" design_component="card-title" is_div="True">
23+
card 3
24+
<container classes="sd-card sd-sphinx-override sd-mb-3 sd-shadow-sm" design_component="card" is_div="True">
25+
<container classes="sd-card-body" design_component="card-body" is_div="True">
26+
<container classes="sd-card-title sd-font-weight-bold" design_component="card-title" is_div="True">
27+
card 4
28+
<container classes="sd-card sd-sphinx-override sd-mb-3 sd-shadow-sm" design_component="card" is_div="True">
29+
<container classes="sd-card-body" design_component="card-body" is_div="True">
30+
<container classes="sd-card-title sd-font-weight-bold" design_component="card-title" is_div="True">
31+
card 5
32+
<container classes="sd-card sd-sphinx-override sd-mb-3 sd-shadow-sm" design_component="card" is_div="True">
33+
<container classes="sd-card-body" design_component="card-body" is_div="True">
34+
<container classes="sd-card-title sd-font-weight-bold" design_component="card-title" is_div="True">
35+
card 6

0 commit comments

Comments
 (0)