diff --git a/backend/static/scss/_timeline.scss b/backend/static/scss/_timeline.scss
index 49f613f..e56b11a 100644
--- a/backend/static/scss/_timeline.scss
+++ b/backend/static/scss/_timeline.scss
@@ -90,6 +90,20 @@ $timeline-circle-size: 15px;
}
}
+// Dark background contrast fix for milestone cards
+// Same pattern as features-section: force white text on dark backgrounds
+.milestone-card {
+ .card {
+ &.bg-dark,
+ &.bg-secondary,
+ &.bg-second-primary,
+ &.bg-dark-green,
+ &.bg-black {
+ color: #fff;
+ }
+ }
+}
+
// Responsive timeline on mobile devices
@include media-breakpoint-down(md) {
// Move the timeline ruler to the left
@@ -115,3 +129,15 @@ $timeline-circle-size: 15px;
left: 0;
}
}
+
+.milestone-text {
+ p {
+ margin-bottom: 0 !important;
+ }
+}
+
+.timeline-text {
+ p {
+ margin-bottom: 0 !important;
+ }
+}
\ No newline at end of file
diff --git a/cms_theme/cms_components.py b/cms_theme/cms_components.py
index 63ba8d6..df1de46 100644
--- a/cms_theme/cms_components.py
+++ b/cms_theme/cms_components.py
@@ -10,8 +10,6 @@
from djangocms_frontend.component_pool import components
from djangocms_frontend.contrib.icon.fields import IconPickerField
from djangocms_frontend.contrib.image.fields import ImageFormField
-
-from .fields import ColorChoiceField
from djangocms_frontend.fields import (
ButtonGroup,
ColoredButtonGroup,
@@ -20,6 +18,8 @@
)
from djangocms_frontend.helpers import first_choice
+from .fields import ColorChoiceField
+
# Common logger
logger = logging.getLogger(__name__)
@@ -197,7 +197,7 @@ class Meta:
render_template = "timeline/timeline.html"
allow_children = True
child_classes = [
- "CardPlugin",
+ "MilestoneCardPlugin",
"TextPlugin",
"HeadingPlugin",
"SpacingPlugin",
@@ -211,6 +211,21 @@ class Meta:
"padding_y": "py-6",
}
+ eyebrow_text = forms.CharField(
+ label=_("Eyebrow text"),
+ required=False,
+ )
+
+ title = forms.CharField(
+ label=_("Title"),
+ required=False,
+ )
+
+ text = HTMLFormField(
+ label=_("Text"),
+ required=False,
+ )
+
divider_color = forms.ChoiceField(
label=_("Divider line color"),
choices=frontend_settings.COLOR_STYLE_CHOICES,
@@ -230,6 +245,44 @@ class Meta:
)
+@components.register
+class MilestoneCard(CMSFrontendComponent):
+ """Milestone card component for Timeline"""
+
+ class Meta:
+ name = _("Milestone Card")
+ render_template = "timeline/milestone_card.html"
+ allow_children = True
+ parent_classes = [
+ "TimelineContainerPlugin",
+ ]
+ mixins = ["Background", "Spacing"]
+ slots = (
+ Slot("links", _("Links"), child_classes=["TextLinkPlugin"]),
+ Slot("image_top", _("Image Top"), child_classes=["ImagePlugin"]),
+ Slot("image_bottom", _("Image Bottom"), child_classes=["ImagePlugin"]),
+ )
+ frontend_editable_fields = ("label", "heading", "text")
+
+ label = forms.CharField(
+ label=_("Milestone label"),
+ required=True,
+ help_text=_("Short label for the milestone, e.g. a year or date."),
+ )
+
+ heading = forms.CharField(
+ label=_("Milestone heading"),
+ required=True,
+ help_text=_("Heading for the milestone card."),
+ )
+
+ text = HTMLFormField(
+ label=_("Milestone text"),
+ required=False,
+ help_text=_("Description text for the milestone."),
+ )
+
+
@components.register
class Footer(CMSFrontendComponent):
"""Footer component with divider color option"""
@@ -1066,6 +1119,7 @@ def clean(self):
@components.register
class CodeBlock(CMSFrontendComponent):
"""Code card component to render code snippets with syntax highlighting"""
+
class Media:
js = (
"admin/vendor/ace/ace.js"
@@ -1198,12 +1252,16 @@ def _fetch_github_stat(self, counter_type):
data = resp.json()
return data.get("stargazers_count" if counter_type == "stars" else "forks_count")
- since = (datetime.now(tz=timezone.utc) - timedelta(days=30)).strftime("%Y-%m-%d")
+ since = (datetime.now(tz=timezone.utc) - timedelta(days=30)).strftime(
+ "%Y-%m-%d"
+ )
if counter_type == "issues_closed":
resp = requests.get(
"https://api.github.com/search/issues",
- params={"q": f"org:{self.GITHUB_ORG} type:issue is:closed closed:>={since}"},
+ params={
+ "q": f"org:{self.GITHUB_ORG} type:issue is:closed closed:>={since}"
+ },
headers={"Accept": "application/vnd.github.v3+json"},
timeout=10,
)
@@ -1213,7 +1271,9 @@ def _fetch_github_stat(self, counter_type):
if counter_type == "merges":
resp = requests.get(
"https://api.github.com/search/issues",
- params={"q": f"org:{self.GITHUB_ORG} type:pr is:merged merged:>={since}"},
+ params={
+ "q": f"org:{self.GITHUB_ORG} type:pr is:merged merged:>={since}"
+ },
headers={"Accept": "application/vnd.github.v3+json"},
timeout=10,
)
@@ -1253,16 +1313,19 @@ class Meta:
child_classes = ["TextLinkPlugin"]
mixins = ["Background", "Attributes"]
fieldsets = (
- (None, {
- "fields": (
- "icon",
- "title",
- ("counter_type", "number", "is_percent"),
- "number_color",
- "description",
- "color_style",
- )
- }),
+ (
+ None,
+ {
+ "fields": (
+ "icon",
+ "title",
+ ("counter_type", "number", "is_percent"),
+ "number_color",
+ "description",
+ "color_style",
+ )
+ },
+ ),
)
counter_type = forms.ChoiceField(
@@ -1301,7 +1364,9 @@ class Meta:
)
def get_short_description(self):
- return dict(COUNTER_TYPE_CHOICES).get(self.config.get("counter_type"), _("Manual"))
+ return dict(COUNTER_TYPE_CHOICES).get(
+ self.config.get("counter_type"), _("Manual")
+ )
@components.register
@@ -1342,7 +1407,7 @@ class Meta:
def get_short_description(self) -> str:
heading = self.config.get("heading")
- background_context = self.config.get('background_context', 'none')
+ background_context = self.config.get("background_context", "none")
if heading:
return f"{heading} ({background_context})"
return background_context
diff --git a/cms_theme/migrations/0007_clientcard_codeblock_countercontainer_heading_and_more.py b/cms_theme/migrations/0007_clientcard_codeblock_countercontainer_heading_and_more.py
new file mode 100644
index 0000000..a2c33cb
--- /dev/null
+++ b/cms_theme/migrations/0007_clientcard_codeblock_countercontainer_heading_and_more.py
@@ -0,0 +1,99 @@
+# Generated by Django 6.0.3 on 2026-03-23 15:38
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cms_theme', '0006_benefitscard_benefitspanel_carouselitem_and_more'),
+ ('djangocms_frontend', '0002_migrate_links'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ClientCard',
+ fields=[
+ ],
+ options={
+ 'verbose_name': 'Client Card',
+ 'managed': False,
+ 'proxy': True,
+ },
+ bases=('djangocms_frontend.frontenduiitem',),
+ ),
+ migrations.CreateModel(
+ name='CodeBlock',
+ fields=[
+ ],
+ options={
+ 'verbose_name': 'Code Block',
+ 'managed': False,
+ 'proxy': True,
+ },
+ bases=('djangocms_frontend.frontenduiitem',),
+ ),
+ migrations.CreateModel(
+ name='CounterContainer',
+ fields=[
+ ],
+ options={
+ 'verbose_name': 'Counter Panel',
+ 'managed': False,
+ 'proxy': True,
+ },
+ bases=('djangocms_frontend.frontenduiitem',),
+ ),
+ migrations.CreateModel(
+ name='Heading',
+ fields=[
+ ],
+ options={
+ 'verbose_name': 'Heading',
+ 'managed': False,
+ 'proxy': True,
+ },
+ bases=('djangocms_frontend.frontenduiitem',),
+ ),
+ migrations.CreateModel(
+ name='MilestoneCard',
+ fields=[
+ ],
+ options={
+ 'verbose_name': 'Milestone Card',
+ 'managed': False,
+ 'proxy': True,
+ },
+ bases=('djangocms_frontend.frontenduiitem',),
+ ),
+ migrations.CreateModel(
+ name='ResponsiveTable',
+ fields=[
+ ],
+ options={
+ 'verbose_name': 'Responsive Table',
+ 'managed': False,
+ 'proxy': True,
+ },
+ bases=('djangocms_frontend.frontenduiitem',),
+ ),
+ migrations.CreateModel(
+ name='Spacing',
+ fields=[
+ ],
+ options={
+ 'verbose_name': 'Spacing',
+ 'managed': False,
+ 'proxy': True,
+ },
+ bases=('djangocms_frontend.frontenduiitem',),
+ ),
+ migrations.AlterModelOptions(
+ name='container1coltext',
+ options={'managed': False, 'verbose_name': 'Single Column'},
+ ),
+ migrations.AlterModelOptions(
+ name='containerwithgrid',
+ options={'managed': False, 'verbose_name': 'Grid Section'},
+ ),
+ ]
diff --git a/cms_theme/templates/timeline/milestone_card.html b/cms_theme/templates/timeline/milestone_card.html
new file mode 100644
index 0000000..0e0ffc2
--- /dev/null
+++ b/cms_theme/templates/timeline/milestone_card.html
@@ -0,0 +1,32 @@
+{% load cms_tags frontend %}
+
+{% inline_field instance "label" %}
+ {% endif %}
+ {% if instance.heading %}
+ {% inline_field instance "heading" %}
+ {% endif %}
+
{{ instance.eyebrow_text }}
+