imdhemy-jekyll-theme 1.2.2 → 1.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50c76c388dec9fe274dcc46260a08128270c6af8b704976dd8f6533cd8a2ce35
4
- data.tar.gz: f10e6dd11fb0deaaa7f3b33da00480c66c25fd41c4f11c724bbefbe6a400b1c3
3
+ metadata.gz: 69e9d0e1c1d690401607c1367a5bc466299ad39b138bec2da357e728051de11b
4
+ data.tar.gz: fadd0e06e18d3555ceabba16456bfcae609feea59ac9d60e60d311c8ccf53b0f
5
5
  SHA512:
6
- metadata.gz: 57973fec87dd5ee464ca42b973d392cac994f3929489906d111a98b8795d6c699944b41e52d20dfa6666cff734e3b1cde9cfa7494bde1631bdfe9eb15b2e767b
7
- data.tar.gz: 1596774bef060733b074cc218c5a6990c09300cbe34f1d0330fc49f505ab47fbeb5eb6b4671d98395048ee5122c2b65a337aa6b1fbb05043b9498f173d8e957a
6
+ metadata.gz: bcb7c70dcd2aa0c7a7455a15f5c5cf75a4e473a63ff16539825c2651cf25397d7011f95315cf2f80bf5307d6a8ba1f08af5075e76a321a8ee54bcc56a389e1fa
7
+ data.tar.gz: bbe0c712aa2bce20a32a6ba998a29dccf93be925dd9427da01b4413fde456305f83363201a1a983a162b85bdf5c8b6ea64affd7c86b1459c43468b24181526f1
data/README.md CHANGED
@@ -10,10 +10,23 @@ All documentation lives in the [`docs/`](./docs) directory.
10
10
  - [Getting Started](./docs/getting-started.md)
11
11
  - [Configuration Reference](./docs/configuration.md)
12
12
  - [Customization Guide](./docs/customization.md)
13
+ - [AI Design System Guide](./docs/ai-design-guide.md)
13
14
  - [Components and Layouts](./docs/components.md)
14
15
  - [Content Elements](./docs/content-elements.md)
15
16
  - [Upgrade Guide](./UPGRADE.md)
16
17
 
18
+ ## Optional Tooling
19
+
20
+ The gem also ships an optional image optimization executable for downstream sites:
21
+
22
+ ```bash
23
+ bundle exec imdhemy-image path/to/image.jpg
24
+ bundle exec imdhemy-image --recursive assets/images
25
+ bundle exec imdhemy-image --dry-run path/to/images
26
+ ```
27
+
28
+ The tool is opt-in and intended for projects using the theme. It is not part of the theme render pipeline.
29
+
17
30
  ## Example Site
18
31
 
19
32
  A complete runnable example is available in [`example/`](./example).
@@ -2,13 +2,13 @@
2
2
  {% assign back_to_posts_label = site.theme_text.back_to_posts_label | default: "Back to all posts" %}
3
3
  <div class="post-header">
4
4
  <a class="post-back-link" href="{{ site.baseurl }}/blog">&larr; {{ back_to_posts_label }}</a>
5
- {% include post-series.html %}
6
5
  <h1 class="post-title">{{ page.title }}</h1>
7
6
  {% include post-meta.html date=page.date reading_minutes=read_minutes class_name="post-header__meta post-meta" %}
8
7
  <div class="post-header__tags">
9
8
  {% assign tags = page.tags %}
10
9
  {% include post-tags.html %}
11
10
  </div>
11
+ {% include post-series.html %}
12
12
 
13
13
  {% if page.image %}
14
14
  <div class="post-image-wrap">
@@ -1,17 +1,40 @@
1
1
  {% assign previous_article_label = site.theme_text.previous_article_label | default: "Previous article" %}
2
2
  {% assign next_article_label = site.theme_text.next_article_label | default: "Next article" %}
3
+ {% assign effective_previous_post = page.previous %}
4
+ {% assign effective_next_post = page.next %}
5
+ {% if page.list %}
6
+ {% assign series_posts = site.posts | where: "list", page.list | sort: "date" %}
7
+ {% assign current_series_index = -1 %}
8
+ {% for post in series_posts %}
9
+ {% if post.url == page.url %}
10
+ {% assign current_series_index = forloop.index0 %}
11
+ {% endif %}
12
+ {% endfor %}
13
+ {% if current_series_index >= 0 %}
14
+ {% assign effective_previous_post = page.previous %}
15
+ {% assign effective_next_post = page.next %}
16
+ {% assign previous_series_index = current_series_index | minus: 1 %}
17
+ {% assign next_series_index = current_series_index | plus: 1 %}
18
+ {% if previous_series_index >= 0 %}
19
+ {% assign effective_previous_post = series_posts[previous_series_index] %}
20
+ {% endif %}
21
+ {% if next_series_index < series_posts.size %}
22
+ {% assign effective_next_post = series_posts[next_series_index] %}
23
+ {% endif %}
24
+ {% endif %}
25
+ {% endif %}
3
26
  <section class="post-navigation-shell">
4
27
  <div class="post-navigation-shell__inner post-navigation">
5
- {% if page.previous.url %}
6
- <a class="post-navigation__item" href="{{ site.baseurl }}{{ page.previous.url }}">
28
+ {% if effective_previous_post.url %}
29
+ <a class="post-navigation__item" href="{{ site.baseurl }}{{ effective_previous_post.url }}">
7
30
  <span class="post-navigation__label">{{ previous_article_label }}</span>
8
- <span class="post-navigation__title">{{ page.previous.title }}</span>
31
+ <span class="post-navigation__title">{{ effective_previous_post.title }}</span>
9
32
  </a>
10
33
  {% endif %}
11
- {% if page.next.url %}
12
- <a class="post-navigation__item post-navigation__item--next" href="{{ site.baseurl }}{{ page.next.url }}">
34
+ {% if effective_next_post.url %}
35
+ <a class="post-navigation__item post-navigation__item--next" href="{{ site.baseurl }}{{ effective_next_post.url }}">
13
36
  <span class="post-navigation__label">{{ next_article_label }}</span>
14
- <span class="post-navigation__title">{{ page.next.title }}</span>
37
+ <span class="post-navigation__title">{{ effective_next_post.title }}</span>
15
38
  </a>
16
39
  {% endif %}
17
40
  </div>
@@ -197,7 +197,7 @@
197
197
  }
198
198
 
199
199
  .hero-section {
200
- padding: 4rem 0;
200
+ padding: 2.25rem 0 2.5rem;
201
201
  background:
202
202
  radial-gradient(circle at 8% 10%, color-mix(in srgb, var(--color-brand) 10%, transparent), transparent 42%),
203
203
  var(--color-surface);
@@ -307,6 +307,10 @@
307
307
  padding: 1.25rem;
308
308
  }
309
309
 
310
+ .latest-posts__inner {
311
+ padding: 0.75rem 0;
312
+ }
313
+
310
314
  .latest-posts__title,
311
315
  .contributions__title,
312
316
  .testimonials__title {
@@ -340,11 +344,20 @@
340
344
  margin-inline: auto;
341
345
  }
342
346
 
347
+ .latest-posts__container {
348
+ width: 100%;
349
+ margin-inline: 0;
350
+ }
351
+
343
352
  .latest-posts__cta {
344
353
  margin-top: 4rem;
345
354
  text-align: center;
346
355
  }
347
356
 
357
+ .latest-posts__list {
358
+ width: min(100% - 2rem, 880px);
359
+ }
360
+
348
361
  .post-listing,
349
362
  .content-shell,
350
363
  .post-header,
@@ -460,9 +473,9 @@
460
473
  .post-card__title {
461
474
  margin-bottom: 0.85rem;
462
475
  font-family: "Manrope", "Helvetica Neue", Helvetica, Arial, sans-serif;
463
- font-size: clamp(1.28rem, 2.3vw, 1.75rem);
476
+ font-size: 1.875rem;
464
477
  font-weight: 800;
465
- line-height: 1.25;
478
+ line-height: 1.2;
466
479
  letter-spacing: -0.01em;
467
480
  text-wrap: balance;
468
481
  overflow-wrap: break-word;
@@ -590,7 +603,7 @@
590
603
 
591
604
  .post-page .post-header {
592
605
  margin-top: 0.4rem;
593
- padding: 1.6rem 1.75rem 1.35rem;
606
+ padding: 1.2rem 1rem 0.95rem;
594
607
  border: 1px solid color-mix(in srgb, var(--color-border) 86%, #c9d6ff 14%);
595
608
  border-bottom: 0;
596
609
  border-radius: 1rem 1rem 0 0;
@@ -782,8 +795,8 @@
782
795
 
783
796
  .post-page .post-title {
784
797
  margin-bottom: 0.95rem;
785
- font-size: clamp(1.85rem, 4.6vw, 3rem);
786
- line-height: 1.16;
798
+ font-size: 1.875rem;
799
+ line-height: 1.2;
787
800
  letter-spacing: -0.014em;
788
801
  }
789
802
 
@@ -803,7 +816,7 @@
803
816
 
804
817
  .post-page .content-shell {
805
818
  margin-top: 0;
806
- padding: 2rem 2rem 1.8rem;
819
+ padding: 1.25rem 1rem 1.1rem;
807
820
  border: 1px solid color-mix(in srgb, var(--color-border) 84%, #c9d6ff 16%);
808
821
  border-top: 0;
809
822
  border-radius: 0 0 1rem 1rem;
@@ -821,20 +834,20 @@
821
834
  .post-page .post-navigation-shell__inner,
822
835
  .post-page .related-posts-shell__inner {
823
836
  margin-inline: auto;
824
- padding: 1.1rem;
837
+ padding: 0.9rem;
825
838
  border: 1px solid color-mix(in srgb, var(--color-border) 84%, #c9d6ff 16%);
826
839
  border-radius: 1rem;
827
840
  background: #fff;
828
841
  }
829
842
 
830
843
  .post-page .comments-shell__inner {
831
- padding: 2rem 2rem 1.8rem;
844
+ padding: 1.25rem 1rem 1.1rem;
832
845
  }
833
846
 
834
847
  .comments-shell__heading {
835
848
  display: block;
836
849
  margin-bottom: 0.3rem;
837
- font-size: 1.3rem;
850
+ font-size: 1.16rem;
838
851
  font-weight: 800;
839
852
  }
840
853
 
@@ -870,8 +883,8 @@
870
883
  display: inline-flex;
871
884
  align-items: center;
872
885
  justify-content: center;
873
- width: 2rem;
874
- height: 2rem;
886
+ width: 1.85rem;
887
+ height: 1.85rem;
875
888
  border: 1px solid color-mix(in srgb, var(--color-border) 84%, #c9d6ff 16%);
876
889
  border-radius: 999px;
877
890
  color: var(--color-muted);
@@ -905,7 +918,7 @@
905
918
  }
906
919
 
907
920
  .post-page .post-navigation {
908
- grid-template-columns: repeat(2, minmax(0, 1fr));
921
+ grid-template-columns: 1fr;
909
922
  gap: 0.9rem;
910
923
  }
911
924
 
@@ -929,7 +942,7 @@
929
942
 
930
943
  .page-article__header {
931
944
  margin-top: 0.4rem;
932
- padding: 1.75rem 1.75rem 1.25rem;
945
+ padding: 1.35rem 1.25rem 1rem;
933
946
  border: 1px solid color-mix(in srgb, var(--color-border) 86%, #c9d6ff 14%);
934
947
  border-bottom: 0;
935
948
  border-radius: 1rem 1rem 0 0;
@@ -939,16 +952,14 @@
939
952
  }
940
953
 
941
954
  .page-article__header-layout {
942
- display: flex;
943
- justify-content: space-between;
944
- gap: 2rem;
955
+ display: block;
945
956
  }
946
957
 
947
958
  .page-article__title {
948
959
  margin: 0 0 1rem;
949
- font-size: clamp(1.9rem, 4.9vw, 3.2rem);
960
+ font-size: clamp(1.35rem, 8.2vw, 1.7rem);
950
961
  font-weight: 800;
951
- line-height: 1.16;
962
+ line-height: 1.2;
952
963
  letter-spacing: -0.014em;
953
964
  text-wrap: balance;
954
965
  overflow-wrap: break-word;
@@ -965,7 +976,7 @@
965
976
 
966
977
  .page-article .content-shell {
967
978
  margin-top: 0;
968
- padding: 2rem 2rem 1.8rem;
979
+ padding: 1.35rem 1.25rem 1.2rem;
969
980
  border: 1px solid color-mix(in srgb, var(--color-border) 84%, #c9d6ff 16%);
970
981
  border-top: 0;
971
982
  border-radius: 0 0 1rem 1rem;
@@ -991,7 +1002,7 @@
991
1002
  .archive-hub .page-header--archive-hub {
992
1003
  margin-top: 0.4rem;
993
1004
  margin-bottom: 0;
994
- padding: 2rem 1.75rem 1.15rem;
1005
+ padding: 1.05rem 1rem 0.8rem;
995
1006
  border: 1px solid color-mix(in srgb, var(--color-border) 86%, #c9d6ff 14%);
996
1007
  border-bottom: 0;
997
1008
  border-radius: 1rem 1rem 0 0;
@@ -1003,7 +1014,7 @@
1003
1014
  .blog-hub .blog-hub__listing,
1004
1015
  .archive-hub .archive-hub__listing {
1005
1016
  margin-top: 0;
1006
- padding: 0.65rem 1.35rem 1.1rem;
1017
+ padding: 0.3rem 0.65rem 0.7rem;
1007
1018
  border: 1px solid color-mix(in srgb, var(--color-border) 84%, #c9d6ff 16%);
1008
1019
  border-top: 0;
1009
1020
  background: #fff;
@@ -1020,7 +1031,7 @@
1020
1031
 
1021
1032
  .blog-hub .pagination-shell {
1022
1033
  margin-top: 0;
1023
- padding: 0 1.35rem 1.2rem;
1034
+ padding: 0 0.65rem 0.75rem;
1024
1035
  border: 1px solid color-mix(in srgb, var(--color-border) 84%, #c9d6ff 16%);
1025
1036
  border-top: 0;
1026
1037
  border-radius: 0 0 1rem 1rem;
@@ -1037,6 +1048,13 @@
1037
1048
  margin-bottom: 0.5rem;
1038
1049
  }
1039
1050
 
1051
+ .blog-hub .page-header__description,
1052
+ .archive-hub .page-header__description {
1053
+ max-width: none;
1054
+ margin-top: 0.6rem;
1055
+ font-size: 1.05rem;
1056
+ }
1057
+
1040
1058
  .page-header-wrap {
1041
1059
  padding: 0;
1042
1060
  background: #fff;
@@ -1047,6 +1065,12 @@
1047
1065
  background: transparent;
1048
1066
  }
1049
1067
 
1068
+ .page-header-wrap--blog .page-header-wrap__container,
1069
+ .page-header-wrap--archive .page-header-wrap__container {
1070
+ width: 100%;
1071
+ margin-inline: 0;
1072
+ }
1073
+
1050
1074
  .page-header {
1051
1075
  margin-inline: auto;
1052
1076
  margin-bottom: 2rem;
@@ -1063,9 +1087,7 @@
1063
1087
  }
1064
1088
 
1065
1089
  .page-header__layout {
1066
- display: flex;
1067
- justify-content: space-between;
1068
- gap: 2rem;
1090
+ display: block;
1069
1091
  padding-inline: 1.25rem;
1070
1092
  }
1071
1093
 
@@ -1075,9 +1097,9 @@
1075
1097
 
1076
1098
  .page-header__title {
1077
1099
  margin-bottom: 2rem;
1078
- font-size: clamp(2.05rem, 7vw, 4.2rem);
1100
+ font-size: clamp(1.65rem, 10vw, 2.25rem);
1079
1101
  font-weight: 700;
1080
- line-height: 1.05;
1102
+ line-height: 1.1;
1081
1103
  letter-spacing: -0.02em;
1082
1104
  text-wrap: balance;
1083
1105
  overflow-wrap: break-word;
@@ -1417,8 +1439,8 @@
1417
1439
  .content {
1418
1440
  color: color-mix(in srgb, var(--color-text) 90%, #1e293b 10%);
1419
1441
  font-family: "Source Serif 4", Georgia, Cambria, "Times New Roman", Times, serif;
1420
- font-size: 1.23rem;
1421
- line-height: 1.9;
1442
+ font-size: 1.12rem;
1443
+ line-height: 1.8;
1422
1444
 
1423
1445
  p {
1424
1446
  margin-bottom: 1.5rem;
@@ -1689,7 +1711,33 @@
1689
1711
  }
1690
1712
  }
1691
1713
 
1714
+ @media (min-width: 421px) {
1715
+ .page-header__title {
1716
+ font-size: clamp(1.85rem, 9vw, 2.8rem);
1717
+ line-height: 1.08;
1718
+ }
1719
+
1720
+ .post-card__title {
1721
+ font-size: 3rem;
1722
+ }
1723
+
1724
+ .post-page .post-title {
1725
+ font-size: 3rem;
1726
+ }
1727
+
1728
+ .page-article__title {
1729
+ font-size: clamp(1.55rem, 7.2vw, 2.25rem);
1730
+ }
1731
+ }
1732
+
1692
1733
  @media (min-width: 1024px) {
1734
+ .page-header-wrap--blog .page-header-wrap__container,
1735
+ .page-header-wrap--archive .page-header-wrap__container,
1736
+ .latest-posts__container {
1737
+ width: min(100% - 2rem, 1280px);
1738
+ margin-inline: auto;
1739
+ }
1740
+
1693
1741
  .site-header__inner {
1694
1742
  padding: 1.5rem 0;
1695
1743
  }
@@ -1707,11 +1755,21 @@
1707
1755
  display: none;
1708
1756
  }
1709
1757
 
1758
+ .hero-section {
1759
+ padding: 4rem 0;
1760
+ }
1761
+
1710
1762
  .hero-description {
1711
1763
  font-size: 1.5rem;
1712
1764
  line-height: 2.5rem;
1713
1765
  }
1714
1766
 
1767
+ .latest-posts__inner,
1768
+ .contributions__inner,
1769
+ .testimonials__inner {
1770
+ padding: 1.25rem;
1771
+ }
1772
+
1715
1773
  .post-card {
1716
1774
  flex-direction: row;
1717
1775
  }
@@ -1724,163 +1782,108 @@
1724
1782
  margin-bottom: 0;
1725
1783
  }
1726
1784
 
1727
- .page-header__layout {
1728
- padding-inline: 0;
1729
- }
1730
-
1731
- .contributions__grid,
1732
- .testimonials__grid {
1733
- grid-template-columns: repeat(3, minmax(0, 1fr));
1734
- }
1735
- }
1736
-
1737
- @media (max-width: 1023px) {
1738
- .page-header-wrap--blog .page-header-wrap__container,
1739
- .page-header-wrap--archive .page-header-wrap__container,
1740
- .latest-posts__container {
1741
- width: 100%;
1742
- margin-inline: 0;
1743
- }
1744
-
1745
- .hero-section {
1746
- padding-top: 2.25rem;
1747
- padding-bottom: 2.5rem;
1748
- }
1749
-
1750
- .page-header__title {
1751
- font-size: clamp(1.85rem, 9vw, 2.8rem);
1752
- line-height: 1.08;
1753
- }
1754
-
1755
1785
  .post-card__title {
1756
- font-size: clamp(1.16rem, 5.8vw, 1.45rem);
1757
- line-height: 1.28;
1758
- }
1759
-
1760
- .post-page .post-title {
1761
- font-size: clamp(1.5rem, 7.4vw, 2.1rem);
1762
- line-height: 1.2;
1763
- }
1764
-
1765
- .page-article__title {
1766
- font-size: clamp(1.55rem, 7.2vw, 2.25rem);
1767
- line-height: 1.2;
1768
- }
1769
-
1770
- .content {
1771
- font-size: 1.12rem;
1772
- line-height: 1.8;
1773
- }
1774
-
1775
- .page-header__layout {
1776
- display: block;
1777
- }
1778
-
1779
- .latest-posts__inner {
1780
- padding: 0.75rem 0;
1781
- }
1782
-
1783
- .latest-posts__list {
1784
- width: min(100% - 2rem, 880px);
1786
+ font-size: 3rem;
1787
+ line-height: 1.25;
1785
1788
  }
1786
1789
 
1787
1790
  .post-page .post-header {
1788
- padding: 1.2rem 1rem 0.95rem;
1791
+ padding: 1.6rem 1.75rem 1.35rem;
1789
1792
  }
1790
1793
 
1791
- .post-series__header,
1792
- .post-series__link,
1793
- .post-series__overflow-toggle {
1794
- padding-left: 0.95rem;
1795
- padding-right: 0.95rem;
1794
+ .post-page .post-title {
1795
+ font-size: 3rem;
1796
+ line-height: 1.16;
1796
1797
  }
1797
1798
 
1798
1799
  .post-page .content-shell {
1799
- padding: 1.25rem 1rem 1.1rem;
1800
+ padding: 2rem 2rem 1.8rem;
1800
1801
  }
1801
1802
 
1802
- .post-page .post-header,
1803
- .post-page .content-shell,
1804
1803
  .post-page .comments-shell__inner,
1805
1804
  .post-page .post-navigation-shell__inner,
1806
1805
  .post-page .related-posts-shell__inner {
1807
- width: min(100% - 2rem, 880px);
1808
- }
1809
-
1810
- .post-page .comments-shell__inner,
1811
- .post-page .post-navigation-shell__inner,
1812
- .post-page .related-posts-shell__inner {
1813
- padding: 0.9rem;
1806
+ padding: 1.1rem;
1814
1807
  }
1815
1808
 
1816
1809
  .post-page .comments-shell__inner {
1817
- padding: 1.25rem 1rem 1.1rem;
1810
+ padding: 2rem 2rem 1.8rem;
1818
1811
  }
1819
1812
 
1820
1813
  .comments-shell__heading {
1821
- font-size: 1.16rem;
1814
+ font-size: 1.3rem;
1822
1815
  }
1823
1816
 
1824
1817
  .comments-collapsible__icon-wrap,
1825
1818
  .series-collapsible__icon-wrap {
1826
- width: 1.85rem;
1827
- height: 1.85rem;
1819
+ width: 2rem;
1820
+ height: 2rem;
1828
1821
  }
1829
1822
 
1830
1823
  .post-page .post-navigation {
1831
- grid-template-columns: 1fr;
1824
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1832
1825
  }
1833
1826
 
1834
1827
  .page-article__header {
1835
- padding: 1.35rem 1.25rem 1rem;
1828
+ padding: 1.75rem 1.75rem 1.25rem;
1836
1829
  }
1837
1830
 
1838
1831
  .page-article__header-layout {
1839
- display: block;
1832
+ display: flex;
1833
+ justify-content: space-between;
1834
+ gap: 2rem;
1835
+ }
1836
+
1837
+ .page-article__title {
1838
+ font-size: clamp(1.9rem, 4.9vw, 3.2rem);
1839
+ line-height: 1.16;
1840
1840
  }
1841
1841
 
1842
1842
  .page-article .content-shell {
1843
- padding: 1.35rem 1.25rem 1.2rem;
1843
+ padding: 2rem 2rem 1.8rem;
1844
1844
  }
1845
1845
 
1846
1846
  .blog-hub .page-header--blog,
1847
1847
  .archive-hub .page-header--archive-hub {
1848
- width: min(100% - 2rem, 880px);
1849
- padding: 1.05rem 1rem 0.8rem;
1848
+ padding: 2rem 1.75rem 1.15rem;
1850
1849
  }
1851
1850
 
1852
1851
  .blog-hub .blog-hub__listing,
1853
1852
  .archive-hub .archive-hub__listing {
1854
- width: min(100% - 2rem, 880px);
1855
- padding: 0.3rem 0.65rem 0.7rem;
1853
+ padding: 0.65rem 1.35rem 1.1rem;
1856
1854
  }
1857
1855
 
1858
1856
  .blog-hub .pagination-shell {
1859
- width: min(100% - 2rem, 880px);
1860
- padding: 0 0.65rem 0.75rem;
1857
+ padding: 0 1.35rem 1.2rem;
1861
1858
  }
1862
1859
 
1863
1860
  .blog-hub .page-header__description,
1864
1861
  .archive-hub .page-header__description {
1865
- max-width: none;
1866
- margin-top: 0.6rem;
1867
- font-size: 1.05rem;
1862
+ max-width: 640px;
1863
+ margin-top: 0;
1864
+ font-size: 1.25rem;
1868
1865
  }
1869
- }
1870
1866
 
1871
- @media (max-width: 420px) {
1872
- .post-card__title {
1873
- font-size: clamp(1.08rem, 7vw, 1.28rem);
1867
+ .page-header__layout {
1868
+ display: flex;
1869
+ justify-content: space-between;
1870
+ gap: 2rem;
1871
+ padding-inline: 0;
1874
1872
  }
1875
1873
 
1876
- .post-page .post-title,
1877
- .page-article__title {
1878
- font-size: clamp(1.35rem, 8.2vw, 1.7rem);
1874
+ .page-header__title {
1875
+ font-size: clamp(2.05rem, 7vw, 4.2rem);
1876
+ line-height: 1.05;
1879
1877
  }
1880
1878
 
1881
- .page-header__title {
1882
- font-size: clamp(1.65rem, 10vw, 2.25rem);
1883
- line-height: 1.1;
1879
+ .contributions__grid,
1880
+ .testimonials__grid {
1881
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1882
+ }
1883
+
1884
+ .content {
1885
+ font-size: 1.23rem;
1886
+ line-height: 1.9;
1884
1887
  }
1885
1888
  }
1886
1889
 
data/exe/imdhemy-image ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/imdhemy/jekyll/theme/image_cli"
5
+
6
+ exit(Imdhemy::Jekyll::Theme::ImageCLI.run(ARGV))
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "optparse"
5
+ require "pathname"
6
+ require "shellwords"
7
+ require "tmpdir"
8
+
9
+ module Imdhemy
10
+ module Jekyll
11
+ module Theme
12
+ class ImageCLI
13
+ SUPPORTED_EXTENSIONS = %w[.jpg .jpeg .png].freeze
14
+
15
+ Result = Struct.new(:path, :status, :original_size, :optimized_size, :message, keyword_init: true)
16
+
17
+ def self.run(argv)
18
+ new(argv).run
19
+ end
20
+
21
+ def initialize(argv)
22
+ @argv = argv.dup
23
+ @options = {
24
+ recursive: false,
25
+ dry_run: false
26
+ }
27
+ end
28
+
29
+ def run
30
+ parser.parse!(@argv)
31
+
32
+ if @argv.empty?
33
+ warn parser.to_s
34
+ return 1
35
+ end
36
+
37
+ paths = expand_inputs(@argv)
38
+ if paths.empty?
39
+ warn "No supported image files found."
40
+ return 1
41
+ end
42
+
43
+ results = paths.map { |path| optimize_path(path) }
44
+ print_results(results)
45
+
46
+ results.any? { |result| result.status == :error } ? 1 : 0
47
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
48
+ warn e.message
49
+ warn parser.to_s
50
+ 1
51
+ end
52
+
53
+ private
54
+
55
+ def parser
56
+ @parser ||= OptionParser.new do |opts|
57
+ opts.banner = "Usage: imdhemy-image [options] PATH [PATH ...]"
58
+
59
+ opts.on("-r", "--recursive", "Scan directories recursively") do
60
+ @options[:recursive] = true
61
+ end
62
+
63
+ opts.on("-n", "--dry-run", "Print what would change without writing files") do
64
+ @options[:dry_run] = true
65
+ end
66
+
67
+ opts.on("-h", "--help", "Show this help") do
68
+ puts opts
69
+ exit 0
70
+ end
71
+ end
72
+ end
73
+
74
+ def expand_inputs(inputs)
75
+ files = inputs.flat_map do |input|
76
+ path = Pathname(input).expand_path
77
+ if path.file?
78
+ supported_file?(path) ? [path] : []
79
+ elsif path.directory?
80
+ collect_directory(path)
81
+ else
82
+ warn "Skipping missing path: #{path}"
83
+ []
84
+ end
85
+ end
86
+
87
+ files.uniq
88
+ end
89
+
90
+ def collect_directory(directory)
91
+ pattern = @options[:recursive] ? "**/*" : "*"
92
+ directory.glob(pattern).select { |path| path.file? && supported_file?(path) }
93
+ end
94
+
95
+ def supported_file?(path)
96
+ SUPPORTED_EXTENSIONS.include?(path.extname.downcase)
97
+ end
98
+
99
+ def optimize_path(path)
100
+ optimizer = optimizer_for(path)
101
+ return Result.new(path: path, status: :skipped, message: "No optimizer available for #{path.extname.downcase}") unless optimizer
102
+
103
+ original_size = path.size
104
+ if @options[:dry_run]
105
+ return Result.new(
106
+ path: path,
107
+ status: :dry_run,
108
+ original_size: original_size,
109
+ optimized_size: original_size,
110
+ message: "Would run #{optimizer.label}"
111
+ )
112
+ end
113
+
114
+ optimized = optimizer.optimize(path)
115
+ return Result.new(path: path, status: :skipped, original_size: original_size, optimized_size: original_size, message: optimized[:message]) unless optimized[:changed]
116
+
117
+ Result.new(
118
+ path: path,
119
+ status: :optimized,
120
+ original_size: original_size,
121
+ optimized_size: path.size,
122
+ message: optimized[:message]
123
+ )
124
+ rescue StandardError => e
125
+ Result.new(path: path, status: :error, original_size: path.exist? ? path.size : nil, message: e.message)
126
+ end
127
+
128
+ def optimizer_for(path)
129
+ case path.extname.downcase
130
+ when ".jpg", ".jpeg"
131
+ jpeg_optimizer
132
+ when ".png"
133
+ png_optimizer
134
+ end
135
+ end
136
+
137
+ def jpeg_optimizer
138
+ @jpeg_optimizer ||= begin
139
+ if command_available?("jpegoptim")
140
+ ExternalOptimizer.new(
141
+ label: "jpegoptim",
142
+ command_builder: lambda { |path, temp_path|
143
+ ["jpegoptim", "--strip-all", "--all-progressive", "--dest=#{temp_path.dirname}", path.to_s]
144
+ },
145
+ temp_output_builder: lambda { |path, temp_dir|
146
+ temp_dir.join(path.basename.to_s)
147
+ }
148
+ )
149
+ elsif command_available?("sips")
150
+ SipsJpegOptimizer.new
151
+ end
152
+ end
153
+ end
154
+
155
+ def png_optimizer
156
+ @png_optimizer ||= begin
157
+ if command_available?("oxipng")
158
+ ExternalOptimizer.new(
159
+ label: "oxipng",
160
+ command_builder: lambda { |path, temp_path|
161
+ ["oxipng", "--strip", "safe", "--opt", "2", "--out", temp_path.to_s, path.to_s]
162
+ },
163
+ temp_output_builder: ->(_path, temp_dir) { temp_dir.join("optimized.png") }
164
+ )
165
+ elsif command_available?("pngcrush")
166
+ ExternalOptimizer.new(
167
+ label: "pngcrush",
168
+ command_builder: lambda { |path, temp_path|
169
+ ["pngcrush", "-q", "-reduce", "-brute", path.to_s, temp_path.to_s]
170
+ },
171
+ temp_output_builder: ->(_path, temp_dir) { temp_dir.join("optimized.png") }
172
+ )
173
+ end
174
+ end
175
+ end
176
+
177
+ def command_available?(command)
178
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |entry|
179
+ executable = File.join(entry, command)
180
+ File.file?(executable) && File.executable?(executable)
181
+ end
182
+ end
183
+
184
+ def print_results(results)
185
+ results.each do |result|
186
+ case result.status
187
+ when :optimized
188
+ saved = result.original_size - result.optimized_size
189
+ percent = percentage(saved, result.original_size)
190
+ puts "optimized #{result.path}: #{human_size(result.original_size)} -> #{human_size(result.optimized_size)} (saved #{percent}%) via #{result.message}"
191
+ when :dry_run
192
+ puts "dry-run #{result.path}: #{result.message}"
193
+ when :skipped
194
+ puts "skipped #{result.path}: #{result.message}"
195
+ when :error
196
+ puts "error #{result.path}: #{result.message}"
197
+ end
198
+ end
199
+ end
200
+
201
+ def percentage(saved, original)
202
+ return 0 if original.to_i <= 0
203
+
204
+ ((saved.to_f / original) * 100).round(1)
205
+ end
206
+
207
+ def human_size(bytes)
208
+ units = %w[B KB MB GB].freeze
209
+ value = bytes.to_f
210
+ unit = units.shift
211
+ while value >= 1024 && !units.empty?
212
+ value /= 1024
213
+ unit = units.shift
214
+ end
215
+
216
+ format(value >= 10 || unit == "B" ? "%.0f %s" : "%.1f %s", value, unit)
217
+ end
218
+ end
219
+
220
+ class ExternalOptimizer
221
+ def initialize(label:, command_builder:, temp_output_builder:)
222
+ @label = label
223
+ @command_builder = command_builder
224
+ @temp_output_builder = temp_output_builder
225
+ end
226
+
227
+ attr_reader :label
228
+
229
+ def optimize(path)
230
+ Dir.mktmpdir("imdhemy-image") do |tmpdir|
231
+ temp_dir = Pathname(tmpdir)
232
+ temp_output = @temp_output_builder.call(path, temp_dir)
233
+ command = @command_builder.call(path, temp_output)
234
+
235
+ success = system(*command, out: File::NULL, err: File::NULL)
236
+ raise "Optimizer failed: #{Shellwords.join(command)}" unless success
237
+
238
+ if !temp_output.exist? || temp_output.size >= path.size
239
+ return { changed: false, message: "#{label} ran but produced no smaller output" }
240
+ end
241
+
242
+ FileUtils.cp(temp_output, path)
243
+ { changed: true, message: label }
244
+ end
245
+ end
246
+ end
247
+
248
+ class SipsJpegOptimizer
249
+ attr_reader :label
250
+
251
+ def initialize
252
+ @label = "sips"
253
+ end
254
+
255
+ def optimize(path)
256
+ Dir.mktmpdir("imdhemy-image") do |tmpdir|
257
+ temp_output = Pathname(tmpdir).join(path.basename.to_s)
258
+ success = system(
259
+ "sips",
260
+ "-s", "format", "jpeg",
261
+ "-s", "formatOptions", "best",
262
+ path.to_s,
263
+ "--out", temp_output.to_s,
264
+ out: File::NULL,
265
+ err: File::NULL
266
+ )
267
+ raise "Optimizer failed: sips" unless success
268
+
269
+ if !temp_output.exist? || temp_output.size >= path.size
270
+ return { changed: false, message: "#{label} ran but produced no smaller output" }
271
+ end
272
+
273
+ FileUtils.cp(temp_output, path)
274
+ { changed: true, message: label }
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imdhemy-jekyll-theme
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mohamad Eldhemy
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-10 00:00:00.000000000 Z
11
+ date: 2026-03-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -69,7 +69,8 @@ dependencies:
69
69
  description:
70
70
  email:
71
71
  - imdhemy@gmail.com
72
- executables: []
72
+ executables:
73
+ - imdhemy-image
73
74
  extensions: []
74
75
  extra_rdoc_files: []
75
76
  files:
@@ -116,6 +117,8 @@ files:
116
117
  - assets/css/main.scss
117
118
  - assets/images/social.png
118
119
  - assets/js/dist/main.js
120
+ - exe/imdhemy-image
121
+ - lib/imdhemy/jekyll/theme/image_cli.rb
119
122
  homepage: https://imdhemy.com
120
123
  licenses:
121
124
  - MIT