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 +4 -4
- data/README.md +13 -0
- data/_includes/post-header.html +1 -1
- data/_includes/post-navigation.html +29 -6
- data/_sass/components/_core.scss +133 -130
- data/exe/imdhemy-image +6 -0
- data/lib/imdhemy/jekyll/theme/image_cli.rb +280 -0
- metadata +7 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69e9d0e1c1d690401607c1367a5bc466299ad39b138bec2da357e728051de11b
|
|
4
|
+
data.tar.gz: fadd0e06e18d3555ceabba16456bfcae609feea59ac9d60e60d311c8ccf53b0f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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).
|
data/_includes/post-header.html
CHANGED
|
@@ -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">← {{ 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
|
|
6
|
-
<a class="post-navigation__item" href="{{ site.baseurl }}{{
|
|
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">{{
|
|
31
|
+
<span class="post-navigation__title">{{ effective_previous_post.title }}</span>
|
|
9
32
|
</a>
|
|
10
33
|
{% endif %}
|
|
11
|
-
{% if
|
|
12
|
-
<a class="post-navigation__item post-navigation__item--next" href="{{ site.baseurl }}{{
|
|
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">{{
|
|
37
|
+
<span class="post-navigation__title">{{ effective_next_post.title }}</span>
|
|
15
38
|
</a>
|
|
16
39
|
{% endif %}
|
|
17
40
|
</div>
|
data/_sass/components/_core.scss
CHANGED
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
.hero-section {
|
|
200
|
-
padding:
|
|
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:
|
|
476
|
+
font-size: 1.875rem;
|
|
464
477
|
font-weight: 800;
|
|
465
|
-
line-height: 1.
|
|
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.
|
|
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:
|
|
786
|
-
line-height: 1.
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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:
|
|
874
|
-
height:
|
|
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:
|
|
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.
|
|
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:
|
|
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.
|
|
960
|
+
font-size: clamp(1.35rem, 8.2vw, 1.7rem);
|
|
950
961
|
font-weight: 800;
|
|
951
|
-
line-height: 1.
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
|
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:
|
|
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(
|
|
1100
|
+
font-size: clamp(1.65rem, 10vw, 2.25rem);
|
|
1079
1101
|
font-weight: 700;
|
|
1080
|
-
line-height: 1.
|
|
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.
|
|
1421
|
-
line-height: 1.
|
|
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:
|
|
1757
|
-
line-height: 1.
|
|
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.
|
|
1791
|
+
padding: 1.6rem 1.75rem 1.35rem;
|
|
1789
1792
|
}
|
|
1790
1793
|
|
|
1791
|
-
.post-
|
|
1792
|
-
|
|
1793
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
1810
|
+
padding: 2rem 2rem 1.8rem;
|
|
1818
1811
|
}
|
|
1819
1812
|
|
|
1820
1813
|
.comments-shell__heading {
|
|
1821
|
-
font-size: 1.
|
|
1814
|
+
font-size: 1.3rem;
|
|
1822
1815
|
}
|
|
1823
1816
|
|
|
1824
1817
|
.comments-collapsible__icon-wrap,
|
|
1825
1818
|
.series-collapsible__icon-wrap {
|
|
1826
|
-
width:
|
|
1827
|
-
height:
|
|
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.
|
|
1828
|
+
padding: 1.75rem 1.75rem 1.25rem;
|
|
1836
1829
|
}
|
|
1837
1830
|
|
|
1838
1831
|
.page-article__header-layout {
|
|
1839
|
-
display:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1866
|
-
margin-top: 0
|
|
1867
|
-
font-size: 1.
|
|
1862
|
+
max-width: 640px;
|
|
1863
|
+
margin-top: 0;
|
|
1864
|
+
font-size: 1.25rem;
|
|
1868
1865
|
}
|
|
1869
|
-
}
|
|
1870
1866
|
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1867
|
+
.page-header__layout {
|
|
1868
|
+
display: flex;
|
|
1869
|
+
justify-content: space-between;
|
|
1870
|
+
gap: 2rem;
|
|
1871
|
+
padding-inline: 0;
|
|
1874
1872
|
}
|
|
1875
1873
|
|
|
1876
|
-
.
|
|
1877
|
-
|
|
1878
|
-
|
|
1874
|
+
.page-header__title {
|
|
1875
|
+
font-size: clamp(2.05rem, 7vw, 4.2rem);
|
|
1876
|
+
line-height: 1.05;
|
|
1879
1877
|
}
|
|
1880
1878
|
|
|
1881
|
-
.
|
|
1882
|
-
|
|
1883
|
-
|
|
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,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.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mohamad Eldhemy
|
|
8
8
|
autorequire:
|
|
9
|
-
bindir:
|
|
9
|
+
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
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
|