imdhemy-jekyll-theme 1.2.1 → 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: a07233cc0c34a281eea8105c4920e6b056f33fc75ca06d6dad4877fd091bcc96
4
- data.tar.gz: 4bbf253795211006906cb6dd4f2d0f1c868ae4fbe831142bbd53e270a6d496dd
3
+ metadata.gz: 69e9d0e1c1d690401607c1367a5bc466299ad39b138bec2da357e728051de11b
4
+ data.tar.gz: fadd0e06e18d3555ceabba16456bfcae609feea59ac9d60e60d311c8ccf53b0f
5
5
  SHA512:
6
- metadata.gz: be9f186c687ff71eb3745cff609f4095923e448f8b70181f2c8f278d5e494c1c559059d6867b0819c1a4c3b43157d700d4332d96ca4bfe6c8b5f7c6a715a54f3
7
- data.tar.gz: bc06729e020dc96cdd5eb19b0fb66e86a65708d8c0f239299c268c43e98639a9f921fa8b2be9c16091cfa6bc2c2692602715e5b730918cadf588484902b4d21e
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);
@@ -231,9 +231,12 @@
231
231
 
232
232
  .hero-title {
233
233
  margin-bottom: 2rem;
234
- font-size: 1.875rem;
234
+ font-size: clamp(1.95rem, 4.2vw, 3rem);
235
235
  font-weight: 700;
236
- line-height: 1.1;
236
+ line-height: 1.12;
237
+ letter-spacing: -0.015em;
238
+ text-wrap: balance;
239
+ overflow-wrap: break-word;
237
240
  }
238
241
 
239
242
  .hero-title__accent {
@@ -304,13 +307,20 @@
304
307
  padding: 1.25rem;
305
308
  }
306
309
 
310
+ .latest-posts__inner {
311
+ padding: 0.75rem 0;
312
+ }
313
+
307
314
  .latest-posts__title,
308
315
  .contributions__title,
309
316
  .testimonials__title {
310
317
  margin-bottom: 2rem;
311
318
  text-align: center;
312
- font-size: 1.875rem;
319
+ font-size: clamp(1.5rem, 2.8vw, 2rem);
313
320
  font-weight: 700;
321
+ line-height: 1.2;
322
+ letter-spacing: -0.01em;
323
+ text-wrap: balance;
314
324
  }
315
325
 
316
326
  .section-subtitle,
@@ -334,11 +344,20 @@
334
344
  margin-inline: auto;
335
345
  }
336
346
 
347
+ .latest-posts__container {
348
+ width: 100%;
349
+ margin-inline: 0;
350
+ }
351
+
337
352
  .latest-posts__cta {
338
353
  margin-top: 4rem;
339
354
  text-align: center;
340
355
  }
341
356
 
357
+ .latest-posts__list {
358
+ width: min(100% - 2rem, 880px);
359
+ }
360
+
342
361
  .post-listing,
343
362
  .content-shell,
344
363
  .post-header,
@@ -455,8 +474,11 @@
455
474
  margin-bottom: 0.85rem;
456
475
  font-family: "Manrope", "Helvetica Neue", Helvetica, Arial, sans-serif;
457
476
  font-size: 1.875rem;
458
- font-weight: 900;
477
+ font-weight: 800;
459
478
  line-height: 1.2;
479
+ letter-spacing: -0.01em;
480
+ text-wrap: balance;
481
+ overflow-wrap: break-word;
460
482
  }
461
483
 
462
484
  .post-card__title-link {
@@ -500,8 +522,12 @@
500
522
 
501
523
  .post-title {
502
524
  font-family: "Manrope", "Helvetica Neue", Helvetica, Arial, sans-serif;
503
- font-size: 1.5rem;
525
+ font-size: clamp(1.45rem, 3vw, 2.1rem);
504
526
  font-weight: 900;
527
+ line-height: 1.18;
528
+ letter-spacing: -0.012em;
529
+ text-wrap: balance;
530
+ overflow-wrap: break-word;
505
531
  }
506
532
 
507
533
  .post-header__meta {
@@ -577,7 +603,7 @@
577
603
 
578
604
  .post-page .post-header {
579
605
  margin-top: 0.4rem;
580
- padding: 1.6rem 1.75rem 1.35rem;
606
+ padding: 1.2rem 1rem 0.95rem;
581
607
  border: 1px solid color-mix(in srgb, var(--color-border) 86%, #c9d6ff 14%);
582
608
  border-bottom: 0;
583
609
  border-radius: 1rem 1rem 0 0;
@@ -769,9 +795,9 @@
769
795
 
770
796
  .post-page .post-title {
771
797
  margin-bottom: 0.95rem;
772
- font-size: clamp(2rem, 4.8vw, 3.25rem);
773
- line-height: 1.15;
774
- letter-spacing: -0.015em;
798
+ font-size: 1.875rem;
799
+ line-height: 1.2;
800
+ letter-spacing: -0.014em;
775
801
  }
776
802
 
777
803
  .post-page .post-image-wrap {
@@ -790,7 +816,7 @@
790
816
 
791
817
  .post-page .content-shell {
792
818
  margin-top: 0;
793
- padding: 2rem 2rem 1.8rem;
819
+ padding: 1.25rem 1rem 1.1rem;
794
820
  border: 1px solid color-mix(in srgb, var(--color-border) 84%, #c9d6ff 16%);
795
821
  border-top: 0;
796
822
  border-radius: 0 0 1rem 1rem;
@@ -808,20 +834,20 @@
808
834
  .post-page .post-navigation-shell__inner,
809
835
  .post-page .related-posts-shell__inner {
810
836
  margin-inline: auto;
811
- padding: 1.1rem;
837
+ padding: 0.9rem;
812
838
  border: 1px solid color-mix(in srgb, var(--color-border) 84%, #c9d6ff 16%);
813
839
  border-radius: 1rem;
814
840
  background: #fff;
815
841
  }
816
842
 
817
843
  .post-page .comments-shell__inner {
818
- padding: 2rem 2rem 1.8rem;
844
+ padding: 1.25rem 1rem 1.1rem;
819
845
  }
820
846
 
821
847
  .comments-shell__heading {
822
848
  display: block;
823
849
  margin-bottom: 0.3rem;
824
- font-size: 1.3rem;
850
+ font-size: 1.16rem;
825
851
  font-weight: 800;
826
852
  }
827
853
 
@@ -857,8 +883,8 @@
857
883
  display: inline-flex;
858
884
  align-items: center;
859
885
  justify-content: center;
860
- width: 2rem;
861
- height: 2rem;
886
+ width: 1.85rem;
887
+ height: 1.85rem;
862
888
  border: 1px solid color-mix(in srgb, var(--color-border) 84%, #c9d6ff 16%);
863
889
  border-radius: 999px;
864
890
  color: var(--color-muted);
@@ -892,7 +918,7 @@
892
918
  }
893
919
 
894
920
  .post-page .post-navigation {
895
- grid-template-columns: repeat(2, minmax(0, 1fr));
921
+ grid-template-columns: 1fr;
896
922
  gap: 0.9rem;
897
923
  }
898
924
 
@@ -916,7 +942,7 @@
916
942
 
917
943
  .page-article__header {
918
944
  margin-top: 0.4rem;
919
- padding: 1.75rem 1.75rem 1.25rem;
945
+ padding: 1.35rem 1.25rem 1rem;
920
946
  border: 1px solid color-mix(in srgb, var(--color-border) 86%, #c9d6ff 14%);
921
947
  border-bottom: 0;
922
948
  border-radius: 1rem 1rem 0 0;
@@ -926,17 +952,17 @@
926
952
  }
927
953
 
928
954
  .page-article__header-layout {
929
- display: flex;
930
- justify-content: space-between;
931
- gap: 2rem;
955
+ display: block;
932
956
  }
933
957
 
934
958
  .page-article__title {
935
959
  margin: 0 0 1rem;
936
- font-size: clamp(2rem, 4.8vw, 3.45rem);
960
+ font-size: clamp(1.35rem, 8.2vw, 1.7rem);
937
961
  font-weight: 800;
938
- line-height: 1.15;
939
- letter-spacing: -0.015em;
962
+ line-height: 1.2;
963
+ letter-spacing: -0.014em;
964
+ text-wrap: balance;
965
+ overflow-wrap: break-word;
940
966
  }
941
967
 
942
968
  .page-article__description {
@@ -950,7 +976,7 @@
950
976
 
951
977
  .page-article .content-shell {
952
978
  margin-top: 0;
953
- padding: 2rem 2rem 1.8rem;
979
+ padding: 1.35rem 1.25rem 1.2rem;
954
980
  border: 1px solid color-mix(in srgb, var(--color-border) 84%, #c9d6ff 16%);
955
981
  border-top: 0;
956
982
  border-radius: 0 0 1rem 1rem;
@@ -976,7 +1002,7 @@
976
1002
  .archive-hub .page-header--archive-hub {
977
1003
  margin-top: 0.4rem;
978
1004
  margin-bottom: 0;
979
- padding: 2rem 1.75rem 1.15rem;
1005
+ padding: 1.05rem 1rem 0.8rem;
980
1006
  border: 1px solid color-mix(in srgb, var(--color-border) 86%, #c9d6ff 14%);
981
1007
  border-bottom: 0;
982
1008
  border-radius: 1rem 1rem 0 0;
@@ -988,7 +1014,7 @@
988
1014
  .blog-hub .blog-hub__listing,
989
1015
  .archive-hub .archive-hub__listing {
990
1016
  margin-top: 0;
991
- padding: 0.65rem 1.35rem 1.1rem;
1017
+ padding: 0.3rem 0.65rem 0.7rem;
992
1018
  border: 1px solid color-mix(in srgb, var(--color-border) 84%, #c9d6ff 16%);
993
1019
  border-top: 0;
994
1020
  background: #fff;
@@ -1005,7 +1031,7 @@
1005
1031
 
1006
1032
  .blog-hub .pagination-shell {
1007
1033
  margin-top: 0;
1008
- padding: 0 1.35rem 1.2rem;
1034
+ padding: 0 0.65rem 0.75rem;
1009
1035
  border: 1px solid color-mix(in srgb, var(--color-border) 84%, #c9d6ff 16%);
1010
1036
  border-top: 0;
1011
1037
  border-radius: 0 0 1rem 1rem;
@@ -1022,6 +1048,13 @@
1022
1048
  margin-bottom: 0.5rem;
1023
1049
  }
1024
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
+
1025
1058
  .page-header-wrap {
1026
1059
  padding: 0;
1027
1060
  background: #fff;
@@ -1032,6 +1065,12 @@
1032
1065
  background: transparent;
1033
1066
  }
1034
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
+
1035
1074
  .page-header {
1036
1075
  margin-inline: auto;
1037
1076
  margin-bottom: 2rem;
@@ -1048,9 +1087,7 @@
1048
1087
  }
1049
1088
 
1050
1089
  .page-header__layout {
1051
- display: flex;
1052
- justify-content: space-between;
1053
- gap: 2rem;
1090
+ display: block;
1054
1091
  padding-inline: 1.25rem;
1055
1092
  }
1056
1093
 
@@ -1060,10 +1097,12 @@
1060
1097
 
1061
1098
  .page-header__title {
1062
1099
  margin-bottom: 2rem;
1063
- font-size: 4.75rem;
1100
+ font-size: clamp(1.65rem, 10vw, 2.25rem);
1064
1101
  font-weight: 700;
1065
- line-height: 1;
1102
+ line-height: 1.1;
1066
1103
  letter-spacing: -0.02em;
1104
+ text-wrap: balance;
1105
+ overflow-wrap: break-word;
1067
1106
  }
1068
1107
 
1069
1108
  .page-header__title--centered {
@@ -1400,8 +1439,8 @@
1400
1439
  .content {
1401
1440
  color: color-mix(in srgb, var(--color-text) 90%, #1e293b 10%);
1402
1441
  font-family: "Source Serif 4", Georgia, Cambria, "Times New Roman", Times, serif;
1403
- font-size: 1.23rem;
1404
- line-height: 1.9;
1442
+ font-size: 1.12rem;
1443
+ line-height: 1.8;
1405
1444
 
1406
1445
  p {
1407
1446
  margin-bottom: 1.5rem;
@@ -1672,7 +1711,33 @@
1672
1711
  }
1673
1712
  }
1674
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
+
1675
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
+
1676
1741
  .site-header__inner {
1677
1742
  padding: 1.5rem 0;
1678
1743
  }
@@ -1690,8 +1755,8 @@
1690
1755
  display: none;
1691
1756
  }
1692
1757
 
1693
- .hero-title {
1694
- font-size: 3rem;
1758
+ .hero-section {
1759
+ padding: 4rem 0;
1695
1760
  }
1696
1761
 
1697
1762
  .hero-description {
@@ -1699,6 +1764,12 @@
1699
1764
  line-height: 2.5rem;
1700
1765
  }
1701
1766
 
1767
+ .latest-posts__inner,
1768
+ .contributions__inner,
1769
+ .testimonials__inner {
1770
+ padding: 1.25rem;
1771
+ }
1772
+
1702
1773
  .post-card {
1703
1774
  flex-direction: row;
1704
1775
  }
@@ -1711,136 +1782,108 @@
1711
1782
  margin-bottom: 0;
1712
1783
  }
1713
1784
 
1714
- .post-title {
1715
- font-size: 2.25rem;
1716
- }
1717
-
1718
- .page-header__layout {
1719
- padding-inline: 0;
1720
- }
1721
-
1722
- .contributions__grid,
1723
- .testimonials__grid {
1724
- grid-template-columns: repeat(3, minmax(0, 1fr));
1725
- }
1726
- }
1727
-
1728
- @media (max-width: 1023px) {
1729
- .page-header-wrap--blog .page-header-wrap__container,
1730
- .page-header-wrap--archive .page-header-wrap__container,
1731
- .latest-posts__container {
1732
- width: 100%;
1733
- margin-inline: 0;
1734
- }
1735
-
1736
- .hero-section {
1737
- padding-top: 2.25rem;
1738
- padding-bottom: 2.5rem;
1739
- }
1740
-
1741
- .page-header__title {
1785
+ .post-card__title {
1742
1786
  font-size: 3rem;
1743
- line-height: 1.05;
1744
- }
1745
-
1746
- .content {
1747
- font-size: 1.12rem;
1748
- line-height: 1.8;
1749
- }
1750
-
1751
- .page-header__layout {
1752
- display: block;
1753
- }
1754
-
1755
- .latest-posts__inner {
1756
- padding: 0.75rem 0;
1757
- }
1758
-
1759
- .latest-posts__list {
1760
- width: min(100% - 2rem, 880px);
1787
+ line-height: 1.25;
1761
1788
  }
1762
1789
 
1763
1790
  .post-page .post-header {
1764
- padding: 1.2rem 1rem 0.95rem;
1791
+ padding: 1.6rem 1.75rem 1.35rem;
1765
1792
  }
1766
1793
 
1767
- .post-series__header,
1768
- .post-series__link,
1769
- .post-series__overflow-toggle {
1770
- padding-left: 0.95rem;
1771
- padding-right: 0.95rem;
1794
+ .post-page .post-title {
1795
+ font-size: 3rem;
1796
+ line-height: 1.16;
1772
1797
  }
1773
1798
 
1774
1799
  .post-page .content-shell {
1775
- padding: 1.25rem 1rem 1.1rem;
1776
- }
1777
-
1778
- .post-page .post-header,
1779
- .post-page .content-shell,
1780
- .post-page .comments-shell__inner,
1781
- .post-page .post-navigation-shell__inner,
1782
- .post-page .related-posts-shell__inner {
1783
- width: min(100% - 2rem, 880px);
1800
+ padding: 2rem 2rem 1.8rem;
1784
1801
  }
1785
1802
 
1786
1803
  .post-page .comments-shell__inner,
1787
1804
  .post-page .post-navigation-shell__inner,
1788
1805
  .post-page .related-posts-shell__inner {
1789
- padding: 0.9rem;
1806
+ padding: 1.1rem;
1790
1807
  }
1791
1808
 
1792
1809
  .post-page .comments-shell__inner {
1793
- padding: 1.25rem 1rem 1.1rem;
1810
+ padding: 2rem 2rem 1.8rem;
1794
1811
  }
1795
1812
 
1796
1813
  .comments-shell__heading {
1797
- font-size: 1.16rem;
1814
+ font-size: 1.3rem;
1798
1815
  }
1799
1816
 
1800
1817
  .comments-collapsible__icon-wrap,
1801
1818
  .series-collapsible__icon-wrap {
1802
- width: 1.85rem;
1803
- height: 1.85rem;
1819
+ width: 2rem;
1820
+ height: 2rem;
1804
1821
  }
1805
1822
 
1806
1823
  .post-page .post-navigation {
1807
- grid-template-columns: 1fr;
1824
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1808
1825
  }
1809
1826
 
1810
1827
  .page-article__header {
1811
- padding: 1.35rem 1.25rem 1rem;
1828
+ padding: 1.75rem 1.75rem 1.25rem;
1812
1829
  }
1813
1830
 
1814
1831
  .page-article__header-layout {
1815
- 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;
1816
1840
  }
1817
1841
 
1818
1842
  .page-article .content-shell {
1819
- padding: 1.35rem 1.25rem 1.2rem;
1843
+ padding: 2rem 2rem 1.8rem;
1820
1844
  }
1821
1845
 
1822
1846
  .blog-hub .page-header--blog,
1823
1847
  .archive-hub .page-header--archive-hub {
1824
- width: min(100% - 2rem, 880px);
1825
- padding: 1.05rem 1rem 0.8rem;
1848
+ padding: 2rem 1.75rem 1.15rem;
1826
1849
  }
1827
1850
 
1828
1851
  .blog-hub .blog-hub__listing,
1829
1852
  .archive-hub .archive-hub__listing {
1830
- width: min(100% - 2rem, 880px);
1831
- padding: 0.3rem 0.65rem 0.7rem;
1853
+ padding: 0.65rem 1.35rem 1.1rem;
1832
1854
  }
1833
1855
 
1834
1856
  .blog-hub .pagination-shell {
1835
- width: min(100% - 2rem, 880px);
1836
- padding: 0 0.65rem 0.75rem;
1857
+ padding: 0 1.35rem 1.2rem;
1837
1858
  }
1838
1859
 
1839
1860
  .blog-hub .page-header__description,
1840
1861
  .archive-hub .page-header__description {
1841
- max-width: none;
1842
- margin-top: 0.6rem;
1843
- font-size: 1.05rem;
1862
+ max-width: 640px;
1863
+ margin-top: 0;
1864
+ font-size: 1.25rem;
1865
+ }
1866
+
1867
+ .page-header__layout {
1868
+ display: flex;
1869
+ justify-content: space-between;
1870
+ gap: 2rem;
1871
+ padding-inline: 0;
1872
+ }
1873
+
1874
+ .page-header__title {
1875
+ font-size: clamp(2.05rem, 7vw, 4.2rem);
1876
+ line-height: 1.05;
1877
+ }
1878
+
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;
1844
1887
  }
1845
1888
  }
1846
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.1
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