core-maugli 1.0.2

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.
Files changed (206) hide show
  1. package/LICENSE +727 -0
  2. package/README.md +108 -0
  3. package/bin/index.js +9 -0
  4. package/bin/init.js +38 -0
  5. package/package.json +73 -0
  6. package/public/blackbox.webp +0 -0
  7. package/public/favicon.svg +10 -0
  8. package/public/footerlabel.svg +18 -0
  9. package/public/icon-192.png +0 -0
  10. package/public/icon-512.png +0 -0
  11. package/public/img/page-images/blog_default.webp +0 -0
  12. package/public/logo-icon.svg +3 -0
  13. package/public/logoblog-icon.svg +10 -0
  14. package/public/manifest.webmanifest +21 -0
  15. package/public/maugli_for_animation.svg +6 -0
  16. package/public/mauglilabel.svg +17 -0
  17. package/public/tr-about-1200.webp +0 -0
  18. package/public/tr-about-400.webp +0 -0
  19. package/public/tr-about-800.webp +0 -0
  20. package/public/tr-about.webp +0 -0
  21. package/public/tr-post-1-1200.webp +0 -0
  22. package/public/tr-post-1-400.webp +0 -0
  23. package/public/tr-post-1-800.webp +0 -0
  24. package/public/tr-post0-1200.webp +0 -0
  25. package/public/tr-post0-400.webp +0 -0
  26. package/public/tr-post0-800.webp +0 -0
  27. package/public/tr-post0.webp +0 -0
  28. package/public/tr-prewiew.webp +0 -0
  29. package/resize-all.cjs +29 -0
  30. package/scripts/featured.js +208 -0
  31. package/scripts/update-with-backup.js +34 -0
  32. package/scripts/upgrade-config.js +90 -0
  33. package/scripts/verify-assets.js +28 -0
  34. package/src/assets/img/default/autor_default.webp +0 -0
  35. package/src/assets/img/default/blog_default.webp +0 -0
  36. package/src/assets/img/default/product_default.webp +0 -0
  37. package/src/assets/img/default/project_default.webp +0 -0
  38. package/src/assets/img/default/rubric_default.webp +0 -0
  39. package/src/assets/img/examples/authors/anna.webp +0 -0
  40. package/src/assets/img/examples/authors/carlos.webp +0 -0
  41. package/src/assets/img/examples/authors/daria.webp +0 -0
  42. package/src/assets/img/examples/authors/dmitry.webp +0 -0
  43. package/src/assets/img/examples/authors/igor.webp +0 -0
  44. package/src/assets/img/examples/authors/john.webp +0 -0
  45. package/src/assets/img/examples/blog/post-1-avtomatizaciya-marketinga-kak-ii-osvobozhdaet-predprinimatelei-ot-cifrovogo-rabstva.webp +0 -0
  46. package/src/assets/img/examples/blog/post_11.webp +0 -0
  47. package/src/assets/img/examples/blog/post_12.webp +0 -0
  48. package/src/assets/img/examples/blog/post_1_jsonld_guide.webp +0 -0
  49. package/src/assets/img/examples/blog/test-post.webp +0 -0
  50. package/src/assets/img/examples/blog/tr-post-1.webp +0 -0
  51. package/src/assets/img/examples/products/product_1.webp +0 -0
  52. package/src/assets/img/examples/products/product_2.webp +0 -0
  53. package/src/assets/img/examples/projects/project_1.webp +0 -0
  54. package/src/assets/img/examples/projects/project_2.webp +0 -0
  55. package/src/components/ArticleMeta.astro +103 -0
  56. package/src/components/AuthorCard.astro +95 -0
  57. package/src/components/AuthorLink.astro +25 -0
  58. package/src/components/AuthorLinksGroup.astro +135 -0
  59. package/src/components/Avatar.astro +32 -0
  60. package/src/components/BaseHead.astro +121 -0
  61. package/src/components/Breadcrumbs.astro +90 -0
  62. package/src/components/Button.astro +25 -0
  63. package/src/components/Card.astro +99 -0
  64. package/src/components/ContentFooter.astro +147 -0
  65. package/src/components/CopyLinkButton.astro +133 -0
  66. package/src/components/CountBadge.astro +17 -0
  67. package/src/components/Footer.astro +122 -0
  68. package/src/components/FormattedDate.astro +59 -0
  69. package/src/components/Header.astro +18 -0
  70. package/src/components/HeroImage.astro +38 -0
  71. package/src/components/IconButton.astro +23 -0
  72. package/src/components/Image.astro +13 -0
  73. package/src/components/InfoCard.astro +123 -0
  74. package/src/components/LanguageSwitcher.astro +122 -0
  75. package/src/components/MaugliFloatingLabel.astro +454 -0
  76. package/src/components/MobileTagsAndShare.astro +71 -0
  77. package/src/components/Nav.astro +151 -0
  78. package/src/components/NavLink.astro +31 -0
  79. package/src/components/Pagination.astro +70 -0
  80. package/src/components/PostPreview.astro +22 -0
  81. package/src/components/ProductBannerCard.astro +47 -0
  82. package/src/components/ProductPreview.astro +21 -0
  83. package/src/components/ProjectPreview.astro +21 -0
  84. package/src/components/RubricCard.astro +142 -0
  85. package/src/components/ShareIcon.astro +93 -0
  86. package/src/components/ShareLink.astro +58 -0
  87. package/src/components/Subscribe.astro +68 -0
  88. package/src/components/SummaryFAQCard.astro +106 -0
  89. package/src/components/TableOfContents.astro +143 -0
  90. package/src/components/TagPill.astro +41 -0
  91. package/src/components/TagPills.astro +42 -0
  92. package/src/components/TagsAndShare.astro +379 -0
  93. package/src/components/TagsSection.astro +203 -0
  94. package/src/components/ThemeToggle.astro +58 -0
  95. package/src/config/maugli.config.ts +213 -0
  96. package/src/content/authors/daria-zorina.md +42 -0
  97. package/src/content/authors/default-autor.md +47 -0
  98. package/src/content/authors/igor-sokolov.md +43 -0
  99. package/src/content/authors/john-walker.md +46 -0
  100. package/src/content/blog/jsonld-guide.md +260 -0
  101. package/src/content/blog/post-0.md +49 -0
  102. package/src/content/blog/post-1-avtomatizaciya-marketinga-kak-ii-osvobozhdaet-predprinimatelei-ot-cifrovogo-rabstva.md +72 -0
  103. package/src/content/blog/post-agent-experience-mcp-biznes-v-epohu-ii-agentov.md +116 -0
  104. package/src/content/blog/test-post-2025-07-11.md +73 -0
  105. package/src/content/config.ts +80 -0
  106. package/src/content/pages/about.md +40 -0
  107. package/src/content/pages/about.mdx +27 -0
  108. package/src/content/pages/authors.mdx +49 -0
  109. package/src/content/pages/blog.mdx +31 -0
  110. package/src/content/pages/contact.md +10 -0
  111. package/src/content/pages/products.mdx +30 -0
  112. package/src/content/pages/projects.mdx +28 -0
  113. package/src/content/pages/rubrics.mdx +35 -0
  114. package/src/content/pages/terms.md +12 -0
  115. package/src/content/products/example-product.md +28 -0
  116. package/src/content/products/maugli-editor.md +35 -0
  117. package/src/content/products/maugli-freeblog.md +162 -0
  118. package/src/content/projects/example-project.md +28 -0
  119. package/src/content/projects/project-1.md +70 -0
  120. package/src/content/projects/project-2.md +33 -0
  121. package/src/content/tags/ai-business.mdx +18 -0
  122. package/src/content/tags/automation.mdx +18 -0
  123. package/src/content/tags/content-strategy.mdx +18 -0
  124. package/src/content/tags/growth-marketing.mdx +18 -0
  125. package/src/content/tags/industry-reviews.mdx +18 -0
  126. package/src/content/tags/interesting.mdx +18 -0
  127. package/src/content/tags/seo-ai-seo.mdx +18 -0
  128. package/src/content.config.ts +260 -0
  129. package/src/data/site-config.ts +164 -0
  130. package/src/i18n/de.json +126 -0
  131. package/src/i18n/en.json +126 -0
  132. package/src/i18n/es.json +126 -0
  133. package/src/i18n/fr.json +126 -0
  134. package/src/i18n/index.ts +10 -0
  135. package/src/i18n/ja.json +126 -0
  136. package/src/i18n/languages.ts +23 -0
  137. package/src/i18n/pt.json +126 -0
  138. package/src/i18n/ru.json +123 -0
  139. package/src/i18n/zh.json +126 -0
  140. package/src/icons/ArrowLeft.astro +13 -0
  141. package/src/icons/ArrowRight.astro +13 -0
  142. package/src/icons/flags/brazil.svg +14 -0
  143. package/src/icons/flags/china.svg +15 -0
  144. package/src/icons/flags/france.svg +12 -0
  145. package/src/icons/flags/germany.svg +12 -0
  146. package/src/icons/flags/japan.svg +11 -0
  147. package/src/icons/flags/russia.svg +12 -0
  148. package/src/icons/flags/spain.svg +12 -0
  149. package/src/icons/flags/united arab emirates.svg +13 -0
  150. package/src/icons/flags/united states.svg +15 -0
  151. package/src/icons/socials/BlueskyIcon.astro +9 -0
  152. package/src/icons/socials/EmailIcon.astro +8 -0
  153. package/src/icons/socials/LinkedinIcon.astro +9 -0
  154. package/src/icons/socials/MastodonIcon.astro +9 -0
  155. package/src/icons/socials/MediumIcon.astro +9 -0
  156. package/src/icons/socials/RedditIcon.astro +11 -0
  157. package/src/icons/socials/TelegramIcon.astro +11 -0
  158. package/src/icons/socials/TwitterIcon.astro +9 -0
  159. package/src/img/default/autor_default.webp +0 -0
  160. package/src/img/default/default.webp +0 -0
  161. package/src/img/default/rubric_default.webp +0 -0
  162. package/src/img/default/test.webp +0 -0
  163. package/src/img/default/test2.webp +0 -0
  164. package/src/img/examples/authors/anna.webp +0 -0
  165. package/src/img/examples/authors/carlos.webp +0 -0
  166. package/src/img/examples/authors/daria.webp +0 -0
  167. package/src/img/examples/authors/dmitry.webp +0 -0
  168. package/src/img/examples/authors/igor.webp +0 -0
  169. package/src/img/examples/authors/john.webp +0 -0
  170. package/src/img/examples/blog/post-1-avtomatizaciya-marketinga-kak-ii-osvobozhdaet-predprinimatelei-ot-cifrovogo-rabstva.webp +0 -0
  171. package/src/img/examples/blog/post-2-avtomatizaciya-kontenta-kak-neiroseti-ubivayut-perfekcionizm-v-biznese.webp +0 -0
  172. package/src/img/examples/blog/post-3-laik-ne-valyuta-kak-avtomatizaciya-marketinga-spasaet-ot-lozhnyh-metrik.webp +0 -0
  173. package/src/img/examples/blog/post-5-5-fatalnyh-oshibok-marketinga-kotorye-ubivayut-startapy-na-starte.webp +0 -0
  174. package/src/img/examples/blog/post-6-5-strategii-kontent-marketinga-dlya-startapov-avtomatizaciya-i-revolyuciya.webp +0 -0
  175. package/src/img/examples/blog/post-7-viralnyi-kontent-ne-udacha-a-strategiya-avtomatizaciya-marketinga.webp +0 -0
  176. package/src/img/examples/blog/post-agent-experience-mcp-biznes-v-epohu-ii-agentov.webp +0 -0
  177. package/src/img/examples/blog/post_1_jsonld_guide.webp +0 -0
  178. package/src/img/examples/blog/tr-post-1.webp +0 -0
  179. package/src/layouts/BaseLayout.astro +59 -0
  180. package/src/pages/404.astro +24 -0
  181. package/src/pages/[...id].astro +50 -0
  182. package/src/pages/about.astro +0 -0
  183. package/src/pages/authors/[...page].astro +105 -0
  184. package/src/pages/authors/[id].astro +165 -0
  185. package/src/pages/blog/[...page].astro +59 -0
  186. package/src/pages/blog/[id].astro +175 -0
  187. package/src/pages/index.astro +90 -0
  188. package/src/pages/products/[...page].astro +50 -0
  189. package/src/pages/products/[id].astro +221 -0
  190. package/src/pages/projects/[...page].astro +74 -0
  191. package/src/pages/projects/[id].astro +165 -0
  192. package/src/pages/projects/tags/[id]/[...page].astro +58 -0
  193. package/src/pages/rss.xml.js +5 -0
  194. package/src/pages/tags/[id]/[...page].astro +110 -0
  195. package/src/pages/tags/index.astro +124 -0
  196. package/src/scripts/infoCardFadeIn.js +22 -0
  197. package/src/styles/global.css +272 -0
  198. package/src/utils/common-utils.ts +0 -0
  199. package/src/utils/content-loader.ts +14 -0
  200. package/src/utils/data-utils.ts +49 -0
  201. package/src/utils/featuredManager.ts +118 -0
  202. package/src/utils/posts.ts +43 -0
  203. package/src/utils/reading-time.ts +28 -0
  204. package/src/utils/remark-slugify.js +8 -0
  205. package/src/utils/rss.ts +23 -0
  206. package/typograf-batch.js +49 -0
@@ -0,0 +1,147 @@
1
+ ---
2
+ import TagPill from './TagPill.astro';
3
+ import Button from './Button.astro';
4
+ import { slugify } from '../utils/common-utils';
5
+ import { maugliConfig } from '../config/maugli.config';
6
+ import { LANGUAGES } from '../i18n/languages';
7
+ export interface Props {
8
+ tags?: string[];
9
+ shareUrl?: string;
10
+ title?: string;
11
+ basePath?: string;
12
+ class?: string;
13
+ productLink?: string;
14
+ }
15
+
16
+ const { tags = [], shareUrl = '', title = '', basePath = '/blog', class: className = '', productLink = '' } = Astro.props;
17
+ const tagsBasePath = basePath === '/projects' ? '/projects/tags' : '/tags';
18
+
19
+ const lang = typeof maugliConfig.defaultLang === 'string' ? maugliConfig.defaultLang : 'en';
20
+ const languageObj = LANGUAGES.find((l) => l.code === lang) || LANGUAGES.find((l) => l.code === 'en');
21
+ const fallbackDict = LANGUAGES.find((l) => l.code === 'en')?.dict || {};
22
+ const dict = languageObj?.dict && Object.keys(languageObj.dict).length > 0 ? languageObj.dict : fallbackDict;
23
+ const buttons = (dict as any).buttons || (fallbackDict as any).buttons || {};
24
+ ---
25
+
26
+ <!-- Блок тегов и кнопки поделиться под контентом (только на мобильных, скрыт на sm и выше) -->
27
+ <div class={`flex flex-wrap items-center gap-6 mt-8 sm:hidden ${className}`}>
28
+ <div class="flex flex-wrap gap-2 w-full">
29
+ {tags.length > 0 && tags.map((tag) => <TagPill tag={tag} href={`${tagsBasePath}/${slugify(tag)}`} />)}
30
+ </div>
31
+ <div class="flex gap-3 w-full">
32
+ <button
33
+ type="button"
34
+ class="share-button mobile-share flex-1 flex justify-center items-center h-12 min-h-12 font-extrabold text-[16px] px-4 py-3 transition-colors duration-200 rounded-custom border border-[var(--border-main)] bg-transparent text-[var(--text-heading)] no-underline"
35
+ aria-label={buttons.share}
36
+ data-url={shareUrl}
37
+ data-title={title}
38
+ data-copied-label={buttons.copied}
39
+ >
40
+ <span class="mobile-share-icon flex items-center justify-center">
41
+ <svg width="20" height="22" viewBox="0 0 20 22" fill="currentColor" xmlns="http://www.w3.org/2000/svg" class="mr-2">
42
+ <g clip-path="url(#clip0_3671_7114)">
43
+ <path
44
+ fill-rule="evenodd"
45
+ clip-rule="evenodd"
46
+ d="M16 0C18.2091 0 20 1.79086 20 4C20 6.20914 18.2091 8 16 8C14.8882 8 13.8831 7.54567 13.1582 6.81348L7.84863 9.91211C7.94625 10.2581 8 10.6227 8 11C8 11.3765 7.94685 11.7405 7.84961 12.0859L13.1611 15.1816C13.8858 14.4517 14.8902 14 16 14C18.2091 14 20 15.7909 20 18C20 20.2091 18.2091 22 16 22C13.7909 22 12 20.2091 12 18C12 17.6214 12.0531 17.2553 12.1514 16.9082L6.84082 13.8135C6.11591 14.5454 5.11153 15 4 15C1.79086 15 0 13.2091 0 11C0 8.79086 1.79086 7 4 7C5.11103 7 6.11598 7.45324 6.84082 8.18457L12.1504 5.08691C12.053 4.74124 12 4.37683 12 4C12 1.79086 13.7909 0 16 0ZM16 16C15.3038 16 14.6912 16.356 14.333 16.8955C14.3183 16.9284 14.3027 16.9613 14.2842 16.9932C14.265 17.026 14.2439 17.0571 14.2217 17.0869C14.0808 17.3608 14 17.6708 14 18C14 19.1046 14.8954 20 16 20C17.1046 20 18 19.1046 18 18C18 16.8954 17.1046 16 16 16ZM4 9C2.89543 9 2 9.89543 2 11C2 12.1046 2.89543 13 4 13C4.73268 13 5.37041 12.6043 5.71875 12.0166C5.72084 12.0129 5.72345 12.0095 5.72559 12.0059C5.73143 11.9958 5.738 11.9863 5.74414 11.9766C5.90629 11.6876 6 11.3549 6 11C6 9.89543 5.10457 9 4 9ZM16 2C14.8954 2 14 2.89543 14 4C14 5.10457 14.8954 6 16 6C17.1046 6 18 5.10457 18 4C18 2.89543 17.1046 2 16 2Z"
47
+ ></path>
48
+ </g>
49
+ <defs>
50
+ <clipPath id="clip0_3671_7114">
51
+ <rect width="20" height="22" fill="white"></rect>
52
+ </clipPath>
53
+ </defs>
54
+ </svg>
55
+ </span>
56
+ {buttons.share}
57
+ </button>
58
+ {
59
+ productLink && (
60
+ <a
61
+ href={productLink}
62
+ target={productLink.startsWith('http') ? '_blank' : undefined}
63
+ rel={productLink.startsWith('http') ? 'noopener noreferrer' : undefined}
64
+ class="flex-1 flex justify-center items-center h-12 min-h-12 font-extrabold text-[16px] px-4 py-3 transition-colors duration-200 rounded-custom bg-[var(--color-brand)] text-[var(--bg-main)] no-underline"
65
+ >
66
+ {buttons.moreAboutProduct}
67
+ </a>
68
+ )
69
+ }
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Десктопный блок -->
74
+ <div class:list={['hidden sm:block md:hidden lg:hidden mt-8', className]}>
75
+ <div class="flex flex-wrap gap-2 w-full">
76
+ {tags.length > 0 && tags.map((tag) => <TagPill tag={tag} href={`${tagsBasePath}/${slugify(tag)}`} />)}
77
+ </div>
78
+ <button
79
+ type="button"
80
+ class="share-button mt-6 flex items-center justify-center h-12 min-h-12 font-extrabold text-[16px] px-4 py-3 rounded-custom border border-[var(--border-main)] bg-transparent text-[var(--text-heading)] no-underline"
81
+ aria-label={buttons.share}
82
+ data-url={shareUrl}
83
+ data-title={title}
84
+ data-copied-label={buttons.copied}
85
+ >
86
+ <svg width="20" height="22" viewBox="0 0 20 22" fill="currentColor" xmlns="http://www.w3.org/2000/svg" class="mr-2">
87
+ <g clip-path="url(#clip0_3671_7114)">
88
+ <path
89
+ fill-rule="evenodd"
90
+ clip-rule="evenodd"
91
+ d="M16 0C18.2091 0 20 1.79086 20 4C20 6.20914 18.2091 8 16 8C14.8882 8 13.8831 7.54567 13.1582 6.81348L7.84863 9.91211C7.94625 10.2581 8 10.6227 8 11C8 11.3765 7.94685 11.7405 7.84961 12.0859L13.1611 15.1816C13.8858 14.4517 14.8902 14 16 14C18.2091 14 20 15.7909 20 18C20 20.2091 18.2091 22 16 22C13.7909 22 12 20.2091 12 18C12 17.6214 12.0531 17.2553 12.1514 16.9082L6.84082 13.8135C6.11591 14.5454 5.11153 15 4 15C1.79086 15 0 13.2091 0 11C0 8.79086 1.79086 7 4 7C5.11103 7 6.11598 7.45324 6.84082 8.18457L12.1504 5.08691C12.053 4.74124 12 4.37683 12 4C12 1.79086 13.7909 0 16 0ZM16 16C15.3038 16 14.6912 16.356 14.333 16.8955C14.3183 16.9284 14.3027 16.9613 14.2842 16.9932C14.265 17.026 14.2439 17.0571 14.2217 17.0869C14.0808 17.3608 14 17.6708 14 18C14 19.1046 14.8954 20 16 20C17.1046 20 18 19.1046 18 18C18 16.8954 17.1046 16 16 16ZM4 9C2.89543 9 2 9.89543 2 11C2 12.1046 2.89543 13 4 13C4.73268 13 5.37041 12.6043 5.71875 12.0166C5.72084 12.0129 5.72345 12.0095 5.72559 12.0059C5.73143 11.9958 5.738 11.9863 5.74414 11.9766C5.90629 11.6876 6 11.3549 6 11C6 9.89543 5.10457 9 4 9ZM16 2C14.8954 2 14 2.89543 14 4C14 5.10457 14.8954 6 16 6C17.1046 6 18 5.10457 18 4C18 2.89543 17.1046 2 16 2Z"
92
+ ></path>
93
+ </g>
94
+ <defs>
95
+ <clipPath id="clip0_3671_7114">
96
+ <rect width="20" height="22" fill="white"></rect>
97
+ </clipPath>
98
+ </defs>
99
+ </svg>
100
+ {buttons.share}
101
+ </button>
102
+ </div>
103
+
104
+ <style>
105
+ .mobile-share {
106
+ /* Уникальный класс для мобильной кнопки */
107
+ }
108
+ .mobile-share-icon {
109
+ min-width: 20px;
110
+ min-height: 22px;
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+ }
115
+ .mobile-share-icon svg {
116
+ width: 20px;
117
+ height: 22px;
118
+ display: block;
119
+ }
120
+ </style>
121
+
122
+ <script>
123
+ document.addEventListener('astro:page-load', () => {
124
+ const shareButtons = document.querySelectorAll('.share-button');
125
+ shareButtons.forEach((button) => {
126
+ button.addEventListener('click', async () => {
127
+ await shareContent(button);
128
+ });
129
+ });
130
+
131
+ async function shareContent(button) {
132
+ const url = button.getAttribute('data-url') || '';
133
+ const title = button.getAttribute('data-title') || '';
134
+ // Только открываем нативный share, не копируем!
135
+ if (navigator.share) {
136
+ try {
137
+ await navigator.share({
138
+ title: title,
139
+ url: url
140
+ });
141
+ } catch (err) {
142
+ // ничего не делаем, не копируем
143
+ }
144
+ }
145
+ }
146
+ });
147
+ </script>
@@ -0,0 +1,133 @@
1
+ ---
2
+ export interface Props {
3
+ url?: string;
4
+ title?: string;
5
+ ariaLabel?: string;
6
+ }
7
+
8
+ const { url = '', title = '', ariaLabel = '' } = Astro.props;
9
+ ---
10
+
11
+ <button class="copy-link-button rounded-circle" title={title} aria-label={ariaLabel} data-url={url}>
12
+ <div class="copyIcon">
13
+ <svg width="100%" height="100%" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
14
+ <path
15
+ fill-rule="evenodd"
16
+ clip-rule="evenodd"
17
+ d="M14 0C21.732 0 28 6.26801 28 14C28 21.732 21.732 28 14 28C6.26801 28 0 21.732 0 14C0 6.26801 6.26801 0 14 0ZM11.3112 12.8618C11.1393 12.8618 10.9742 12.93 10.8526 13.0515L8.34611 15.5649C7.84129 16.1144 7.56815 16.8377 7.5839 17.5837C7.5997 18.3299 7.90325 19.0413 8.43099 19.569C8.95873 20.0968 9.67009 20.4003 10.4163 20.4161C11.1623 20.4319 11.8856 20.1587 12.4351 19.6539L14.9485 17.1474C15.07 17.0258 15.1382 16.8607 15.1382 16.6888C15.1382 16.5169 15.07 16.3518 14.9485 16.2302C14.8268 16.1086 14.6614 16.04 14.4893 16.04C14.3174 16.04 14.1523 16.1086 14.0308 16.2302L11.5243 18.7367C11.2208 19.0295 10.8157 19.193 10.394 19.193C9.9723 19.193 9.56673 19.0296 9.26326 18.7367C9.11447 18.5885 8.99635 18.4123 8.91577 18.2183C8.83519 18.0243 8.79386 17.816 8.79386 17.606C8.79387 17.396 8.83526 17.1881 8.91577 16.9941C8.99631 16.8002 9.11454 16.6239 9.26326 16.4757L11.7698 13.9692C11.8914 13.8476 11.96 13.6826 11.96 13.5107C11.96 13.3386 11.8914 13.1732 11.7698 13.0515C11.6482 12.93 11.4831 12.8618 11.3112 12.8618ZM15.5489 11.8022C15.3769 11.8023 15.212 11.8709 15.0903 11.9925L11.9122 15.1707C11.8517 15.2307 11.8037 15.3022 11.7709 15.3809C11.7381 15.4596 11.7208 15.544 11.7208 15.6292C11.7208 15.7145 11.7381 15.7989 11.7709 15.8776C11.8037 15.9563 11.8516 16.0278 11.9122 16.0878C11.9726 16.1477 12.0443 16.1952 12.123 16.2274C12.2016 16.2595 12.2858 16.2757 12.3708 16.2752C12.4557 16.2757 12.5399 16.2595 12.6186 16.2274C12.6973 16.1952 12.769 16.1477 12.8293 16.0878L16.0075 12.9097C16.1291 12.788 16.1977 12.6231 16.1978 12.4511C16.1978 12.2791 16.1291 12.1142 16.0075 11.9925C15.8858 11.8709 15.7209 11.8022 15.5489 11.8022ZM17.5291 7.58333C16.763 7.58333 16.0279 7.88645 15.4845 8.42643L12.9718 10.9329C12.9116 10.9931 12.8636 11.0645 12.8311 11.1431C12.7985 11.2218 12.7815 11.3064 12.7815 11.3915C12.7815 11.4767 12.7985 11.5612 12.8311 11.6399C12.8636 11.7185 12.9116 11.7899 12.9718 11.8501C13.032 11.9103 13.1033 11.9582 13.182 11.9908C13.2607 12.0234 13.3452 12.0404 13.4303 12.0404C13.5154 12.0403 13.5996 12.0233 13.6781 11.9908C13.7568 11.9582 13.8287 11.9103 13.8889 11.8501L16.3954 9.34359C16.6988 9.05085 17.104 8.88731 17.5256 8.88729C17.9474 8.88729 18.353 9.05071 18.6564 9.34359C18.8052 9.4918 18.9233 9.66803 19.0039 9.86198C19.0845 10.056 19.1258 10.2643 19.1258 10.4744C19.1258 10.6843 19.0844 10.8923 19.0039 11.0862C18.9234 11.2801 18.8052 11.4564 18.6564 11.6046L16.1499 14.1111C16.0894 14.1711 16.0414 14.2427 16.0086 14.3213C15.9759 14.3999 15.9585 14.4844 15.9585 14.5697C15.9585 14.6549 15.9759 14.7393 16.0086 14.818C16.0414 14.8967 16.0894 14.9682 16.1499 15.0282C16.21 15.0888 16.2814 15.1373 16.3601 15.1701C16.4388 15.2028 16.5233 15.2196 16.6085 15.2196C16.6937 15.2196 16.7782 15.2029 16.8569 15.1701C16.9356 15.1373 17.007 15.0888 17.0671 15.0282L19.5736 12.5155C20.1135 11.9721 20.4167 11.237 20.4167 10.4709C20.4166 9.70486 20.1136 8.96983 19.5736 8.42643C19.0302 7.88641 18.2951 7.58336 17.5291 7.58333Z"
18
+ fill="var(--text-main)"
19
+ fill-opacity="0.8"></path>
20
+ </svg>
21
+ </div>
22
+ </button>
23
+
24
+ <style>
25
+ .copy-link-button {
26
+ border: none;
27
+ cursor: pointer;
28
+ padding: 0;
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ transition: transform 0.2s ease;
33
+ text-decoration: none;
34
+ width: 28px;
35
+ height: 28px;
36
+ color: var(--bg-main);
37
+ }
38
+
39
+ .copy-link-button:hover {
40
+ transform: scale(1.1);
41
+ }
42
+
43
+ .copyIcon {
44
+ width: 28px;
45
+ height: 28px;
46
+ display: flex;
47
+ align-items: center;
48
+ justify-content: center;
49
+ }
50
+
51
+ .copyIcon svg {
52
+ width: 100%;
53
+ height: 100%;
54
+ }
55
+
56
+ /* Планшеты */
57
+ @media (min-width: 768px) and (max-width: 1023px) {
58
+ .copy-link-button {
59
+ width: 28px;
60
+ height: 28px;
61
+ }
62
+ .copyIcon {
63
+ width: 28px;
64
+ height: 28px;
65
+ }
66
+ }
67
+
68
+ /* Десктоп */
69
+ @media (min-width: 1024px) {
70
+ .copy-link-button {
71
+ width: 36px;
72
+ height: 36px;
73
+ }
74
+ .copyIcon {
75
+ width: 36px;
76
+ height: 36px;
77
+ }
78
+ }
79
+ </style>
80
+
81
+ <script>
82
+ document.addEventListener('astro:page-load', () => {
83
+ // Обработчик для кнопки копирования ссылки
84
+ const copyButtons = document.querySelectorAll('.copy-link-button');
85
+
86
+ copyButtons.forEach((button) => {
87
+ button.addEventListener('click', async (e) => {
88
+ e.preventDefault();
89
+ const url = button.getAttribute('data-url') || window.location.href;
90
+
91
+ try {
92
+ await navigator.clipboard.writeText(url);
93
+
94
+ // Попытка воспроизвести системный звук копирования
95
+ try {
96
+ // Создаем короткий звуковой сигнал
97
+ const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
98
+ const oscillator = audioContext.createOscillator();
99
+ const gainNode = audioContext.createGain();
100
+
101
+ oscillator.connect(gainNode);
102
+ gainNode.connect(audioContext.destination);
103
+
104
+ oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
105
+ gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
106
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
107
+
108
+ oscillator.start(audioContext.currentTime);
109
+ oscillator.stop(audioContext.currentTime + 0.1);
110
+ } catch (audioErr) {
111
+ console.log('Audio context not available');
112
+ }
113
+
114
+ // Временно меняем иконку на галочку
115
+ const iconDiv = button.querySelector('.copyIcon');
116
+ if (iconDiv) {
117
+ const originalContent = iconDiv.innerHTML;
118
+ iconDiv.innerHTML = `<svg width="100%" height="100%" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
119
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M14 0C21.732 0 28 6.26801 28 14C28 21.732 21.732 28 14 28C6.26801 28 0 21.732 0 14C0 6.26801 6.26801 0 14 0ZM12.9307 16.8096L8 11.8789L5.87891 14L11.9395 20.0605C12.2322 20.3533 12.633 20.5119 13.0469 20.499C13.4607 20.4861 13.8511 20.3027 14.125 19.9922L22.6172 10.3672L20.3672 8.38281L12.9307 16.8096Z" fill="var(--text-main)" fill-opacity="0.8"/>
120
+ </svg>`;
121
+
122
+ // Возвращаем исходную иконку через 2 секунды
123
+ setTimeout(() => {
124
+ iconDiv.innerHTML = originalContent;
125
+ }, 2000);
126
+ }
127
+ } catch (err) {
128
+ console.error('Failed to copy URL:', err);
129
+ }
130
+ });
131
+ });
132
+ });
133
+ </script>
@@ -0,0 +1,17 @@
1
+ ---
2
+ // Компонент для бейджа-счётчика (например, количество постов у автора или в рубрике)
3
+ // Используется и в AuthorCard, и в RubricCard для консистентности
4
+ export interface Props {
5
+ count: number;
6
+ class?: string;
7
+ }
8
+
9
+ const { count, class: className = '' } = Astro.props;
10
+ ---
11
+
12
+ <span
13
+ class={`ml-2 px-2 py-0.5 rounded-normal border border-[var(--border-main)] bg-[var(--card-bg)] text-[var(--text-muted)] text-base font-semibold inline-block align-middle ${className}`}
14
+ style="vertical-align: middle;"
15
+ >
16
+ {count}
17
+ </span>
@@ -0,0 +1,122 @@
1
+ ---
2
+ import { maugliConfig } from '../config/maugli.config';
3
+ import { LANGUAGES } from '../i18n/languages';
4
+ import AuthorLinksGroup from './AuthorLinksGroup.astro';
5
+ // Универсальный импорт словарей по доступным языкам
6
+ const dicts: Record<string, any> = {};
7
+ for (const lang of LANGUAGES) {
8
+ try {
9
+ dicts[lang.code] = await import(`../i18n/${lang.code}.json`).then((m) => m.default);
10
+ } catch {}
11
+ }
12
+ const lang = maugliConfig.lang || maugliConfig.defaultLang || 'en';
13
+ const dict = dicts[lang] || dicts['en'] || {};
14
+
15
+ const navLinks = (maugliConfig.navLinks || []).map((link) => ({
16
+ ...link,
17
+ label: dict.nav?.[link.key] || link.label
18
+ }));
19
+ const yearStart = 2025;
20
+ const yearEnd = 2025;
21
+ const copyrightYears = yearStart === yearEnd ? `${yearStart}` : `${yearStart}–${yearEnd}`;
22
+ ---
23
+
24
+ <footer class="w-full pt-12 pb-10 mb-6 sm:pt-24 sm:pb-14">
25
+ <style>
26
+ .footer-link {
27
+ transition: color 0.2s;
28
+ }
29
+ .footer-link:hover {
30
+ color: var(--brand-color);
31
+ text-decoration: none;
32
+ }
33
+ .rss-outline {
34
+ border: 1.5px solid var(--text-muted);
35
+ border-radius: 50%;
36
+ width: 36px;
37
+ height: 36px;
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: center;
41
+ transition: border-color 0.2s;
42
+ background: none;
43
+ }
44
+ .rss-outline svg {
45
+ color: var(--text-muted);
46
+ stroke: var(--text-muted);
47
+ }
48
+ .rss-outline:hover {
49
+ border-color: var(--brand-color);
50
+ }
51
+ </style>
52
+ {
53
+ navLinks.length > 0 && (
54
+ <div class="mb-4 flex flex-wrap gap-x-6 gap-y-1 justify-between items-center">
55
+ <div class="flex flex-wrap gap-x-6 gap-y-1 items-center">
56
+ {lang === 'ru' ? (
57
+ <a
58
+ class="footer-link flex items-center text-muted mr-2"
59
+ href="/rss.xml"
60
+ target="_blank"
61
+ rel="noopener"
62
+ style="color:var(--text-heading);"
63
+ >
64
+ RSS
65
+ </a>
66
+ ) : (
67
+ <a class="footer-link flex items-center mr-2" href="/rss.xml" aria-label="RSS" target="_blank" rel="noopener">
68
+ <svg
69
+ width="20"
70
+ height="20"
71
+ viewBox="0 0 22 22"
72
+ fill="none"
73
+ stroke="var(--text-heading)"
74
+ stroke-width="1.7"
75
+ stroke-linecap="round"
76
+ stroke-linejoin="round"
77
+ style="display:inline;vertical-align:middle;"
78
+ >
79
+ <circle cx="6.18" cy="17.82" r="2.18" fill="none" stroke="var(--text-heading)" stroke-width="1.7" />
80
+ <path d="M4 4a16 16 0 0 1 16 16" />
81
+ <path d="M4 11a9 9 0 0 1 9 9" />
82
+ </svg>
83
+ </a>
84
+ )}
85
+ {navLinks.map((link) => (
86
+ <a class="footer-link font-serif text-heading" href={link.href}>
87
+ {link.label}
88
+ </a>
89
+ ))}
90
+ </div>
91
+ <div class="hidden sm:block text-sm font-serif" style="color:var(--text-muted);text-align:right;opacity:0.6;">
92
+ {maugliConfig.brand.name} © {copyrightYears}. {dict.footer.allRightsReserved}
93
+ </div>
94
+ </div>
95
+ )
96
+ }
97
+ {
98
+ navLinks.length > 0 && (
99
+ <div class="block sm:hidden w-full mt-2 text-sm font-serif" style="color:var(--text-muted);text-align:left;opacity:0.6;">
100
+ {maugliConfig.brand.name} © {copyrightYears}. {dict.footer.allRightsReserved}
101
+ </div>
102
+ )
103
+ }
104
+ <div class="block sm:hidden mt-4"></div>
105
+ <div
106
+ class:list={[
107
+ 'pt-6 flex flex-col gap-4 border-t border-solid border-main',
108
+ { 'sm:flex-row-reverse sm:justify-between sm:items-center': Object.values(maugliConfig.links || {}).some(Boolean) }
109
+ ]}
110
+ >
111
+ <div class="flex flex-row justify-between items-end w-full mt-2">
112
+ <img src="/footerlabel.svg" alt="Maugli Label" style="height:32px;width:auto;" />
113
+ {
114
+ Object.values(maugliConfig.links || {}).some(Boolean) && (
115
+ <div class="flex flex-row items-center gap-2 ml-4">
116
+ <AuthorLinksGroup socials={maugliConfig.links} authorName={maugliConfig.brand.name} maxLinks={8} />
117
+ </div>
118
+ )
119
+ }
120
+ </div>
121
+ </div>
122
+ </footer>
@@ -0,0 +1,59 @@
1
+ ---
2
+ import { maugliConfig } from '../config/maugli.config';
3
+ import { LANGUAGES } from '../i18n/languages';
4
+ // Универсальный импорт словарей по доступным языкам
5
+ const dicts: Record<string, any> = {};
6
+ for (const lang of LANGUAGES) {
7
+ try {
8
+ dicts[lang.code] = await import(`../i18n/${lang.code}.json`).then((m) => m.default);
9
+ } catch {}
10
+ }
11
+
12
+ type Props = { date: Date };
13
+
14
+ const { date } = Astro.props;
15
+ const lang = maugliConfig.defaultLang || 'en';
16
+ const dict = dicts[lang] || dicts['en'] || {};
17
+ const locale = lang;
18
+
19
+ // Получаем текущую дату
20
+ const now = new Date();
21
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
22
+ const yesterday = new Date(today);
23
+ yesterday.setDate(yesterday.getDate() - 1);
24
+
25
+ // Приводим переданную дату к началу дня для корректного сравнения
26
+ const inputDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
27
+
28
+ // Определяем формат отображения
29
+ let displayText: string;
30
+
31
+ if (inputDate.getTime() === today.getTime()) {
32
+ displayText = dict.date.today;
33
+ } else if (inputDate.getTime() === yesterday.getTime()) {
34
+ displayText = dict.date.yesterday;
35
+ } else {
36
+ // Для остальных дат
37
+ const currentYear = now.getFullYear();
38
+ const dateYear = date.getFullYear();
39
+
40
+ if (dateYear === currentYear) {
41
+ // Текущий год - не показываем год
42
+ displayText = date.toLocaleDateString(locale, {
43
+ month: 'long',
44
+ day: 'numeric'
45
+ });
46
+ } else {
47
+ // Другой год - показываем год
48
+ displayText = date.toLocaleDateString(locale, {
49
+ year: 'numeric',
50
+ month: 'long',
51
+ day: 'numeric'
52
+ });
53
+ }
54
+ }
55
+ ---
56
+
57
+ <time datetime={date.toISOString()}>
58
+ {displayText}
59
+ </time>
@@ -0,0 +1,18 @@
1
+ ---
2
+ import siteConfig from '../data/site-config';
3
+ ---
4
+
5
+ <header class="w-full mb-12 sm:mb-16">
6
+ {
7
+ siteConfig.logo && siteConfig.logo?.src ? (
8
+ <a href="/">
9
+ <img src={siteConfig.logo.src} alt={siteConfig.logo.alt || ''} class="max-h-12" />
10
+ </a>
11
+ ) : (
12
+ <a class="font-serif text-2xl leading-tight font-medium text-theme-foreground sm:text-4xl" href="/">
13
+ {siteConfig.title}
14
+ </a>
15
+ )
16
+ }
17
+ {siteConfig.subtitle && <p class="text-sm leading-tight mt-1">{siteConfig.subtitle}</p>}
18
+ </header>
@@ -0,0 +1,38 @@
1
+ ---
2
+ // src/components/HeroImage.astro
3
+ export interface Props {
4
+ src: string;
5
+ alt?: string;
6
+ caption?: string;
7
+ width?: number | string;
8
+ height?: number | string;
9
+ className?: string;
10
+ srcset?: string;
11
+ }
12
+ const defaultWidth = 800;
13
+ const { src, alt = '', caption, width = defaultWidth, height, className = '', srcset = '' } = Astro.props;
14
+
15
+ // Вариант aspect-ratio: если есть оба размера, рассчитываем; иначе не пишем!
16
+ const aspectRatio = width && height ? `${width} / ${height}` : undefined;
17
+
18
+ let realSrcset = srcset;
19
+ if (!srcset && src.includes('default-')) {
20
+ realSrcset = `${src.replace('800', '800')} 1x, ${src.replace('800', '1200')} 2x`;
21
+ }
22
+ ---
23
+
24
+ <figure class={`w-full max-w-[${width}px] mx-auto rounded-custom overflow-hidden bg-[var(--bg-muted)] ${className}`}>
25
+ <img
26
+ src={src}
27
+ alt={alt}
28
+ width={width}
29
+ height={height}
30
+ style={aspectRatio ? `aspect-ratio: ${aspectRatio}; width: 100%; height: auto;` : 'width: 100%; height: auto;'}
31
+ class="block rounded-custom object-cover"
32
+ loading="eager"
33
+ decoding="async"
34
+ srcset={realSrcset}
35
+ sizes="100vw"
36
+ />
37
+ {caption && <figcaption class="mt-2 text-sm text-[var(--text-main)] opacity-80 text-center px-2">{caption}</figcaption>}
38
+ </figure>
@@ -0,0 +1,23 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types';
3
+
4
+ type AnchorProps = HTMLAttributes<'a'> & { type?: never };
5
+ type ButtonProps = HTMLAttributes<'button'> & { href?: never };
6
+
7
+ type Props = ButtonProps | AnchorProps;
8
+
9
+ const { href, class: className, ...rest } = Astro.props;
10
+ const buttonClasses = 'inline-flex items-center justify-center p-2 text-main bg-main border border-main rounded-custom transition hover:bg-muted';
11
+ ---
12
+
13
+ {
14
+ href ? (
15
+ <a href={href} class:list={[buttonClasses, className]} {...rest}>
16
+ <slot />
17
+ </a>
18
+ ) : (
19
+ <button class:list={[buttonClasses, 'cursor-pointer', className]} {...rest}>
20
+ <slot />
21
+ </button>
22
+ )
23
+ }
@@ -0,0 +1,13 @@
1
+ ---
2
+ export let src = '';
3
+ export let alt = '';
4
+ export let width = undefined;
5
+ export let height = undefined;
6
+ export let className = '';
7
+ export let loading = 'lazy';
8
+ export let style = '';
9
+ ---
10
+
11
+ export let height = undefined; export let className = ''; export let loading = 'lazy'; export let style = ''; ---
12
+
13
+ <img src={src} alt={alt} width={width} height={height} class={className} loading={loading} style={style} />