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,58 @@
1
+ ---
2
+ import ShareIcon from './ShareIcon.astro';
3
+ export interface Props {
4
+ url: string;
5
+ platform: 'twitter' | 'telegram' | 'linkedin' | 'whatsapp' | 'facebook' | 'copy' | 'copied';
6
+ icon: string;
7
+ name?: string;
8
+ }
9
+ const { url, platform, icon, name = '' } = Astro.props;
10
+ ---
11
+
12
+ <a
13
+ class="share-link share-button"
14
+ href={url}
15
+ target="_blank"
16
+ rel="noopener noreferrer external"
17
+ title={name || platform}
18
+ aria-label={name || platform}
19
+ tabindex="0"
20
+ >
21
+ <ShareIcon platform={platform} />
22
+ </a>
23
+
24
+ <style>
25
+ .share-link {
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ border-radius: 50%;
30
+ border: none;
31
+ background: var(--text-main);
32
+ color: var(--bg-main);
33
+ width: 28px;
34
+ height: 28px;
35
+ transition: transform 0.2s ease;
36
+ text-decoration: none;
37
+ cursor: pointer;
38
+ padding: 0;
39
+ }
40
+ .share-link:hover {
41
+ transform: scale(1.1);
42
+ }
43
+ .share-link svg {
44
+ width: 100%;
45
+ height: 100%;
46
+ fill: currentColor;
47
+ }
48
+ @media (min-width: 1024px) {
49
+ .share-link {
50
+ width: 36px;
51
+ height: 36px;
52
+ }
53
+ }
54
+ html[data-theme='dark'] .share-link {
55
+ background: var(--bg-main);
56
+ color: var(--text-main);
57
+ }
58
+ </style>
@@ -0,0 +1,68 @@
1
+ ---
2
+ import { maugliConfig } from '../config/maugli.config';
3
+ import { LANGUAGES } from '../i18n/languages';
4
+ import Button from './Button.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
+
13
+ const subscribe = maugliConfig.subscribe;
14
+ const { class: className } = Astro.props;
15
+ const lang: string = maugliConfig.defaultLang;
16
+ const dict = dicts[lang] || dicts['en'] || {};
17
+
18
+ const heading = subscribe.heading || dict.subscribe?.heading || dicts['en']?.subscribe?.heading;
19
+ const mutedText = subscribe.mutedText || dict.subscribe?.mutedText || dicts['en']?.subscribe?.mutedText;
20
+ ---
21
+
22
+ {
23
+ subscribe?.formUrl && (
24
+ <div class:list={['max-w-[1280px] mx-auto', className]}>
25
+ <section class="px-8 py-12 flex flex-col items-center border border-solid border-main text-center rounded-custom sm:px-12 sm:py-16 my-16 sm:my-24">
26
+ {heading && (
27
+ <h2 style="color:var(--text-heading)" class="w-full text-2xl leading-tight font-serif font-black sm:text-4xl mb-4">
28
+ {heading}
29
+ </h2>
30
+ )}
31
+ {mutedText && (
32
+ <p style="color:var(--text-muted)" class="w-full max-w-xl mb-8 text-sm leading-normal md:text-base">
33
+ {mutedText}
34
+ </p>
35
+ )}
36
+ <form
37
+ action={subscribe.formUrl}
38
+ method="post"
39
+ id="subscribe-form"
40
+ name="subscribe-form"
41
+ aria-label={dict.form?.subscribeAriaLabel || en.form.subscribeAriaLabel}
42
+ class="w-full max-w-xl flex flex-col gap-3.5 sm:flex-row"
43
+ target="_blank"
44
+ >
45
+ <label for="email" class="sr-only">
46
+ {dict.form?.emailLabel || en.form.emailLabel}
47
+ </label>
48
+ <input
49
+ type="email"
50
+ name="email"
51
+ id="email"
52
+ class="flex-1 h-11 px-5 py-2 text-sm border border-solid border-main rounded-custom placeholder:opacity-50 focus:outline-none focus:ring-2 focus:ring-brand focus:border-transparent"
53
+ required=""
54
+ value=""
55
+ placeholder={dict.form?.emailPlaceholder || en.form.emailPlaceholder}
56
+ />
57
+ <Button
58
+ type="submit"
59
+ name="subscribe"
60
+ class="w-full h-11 sm:w-auto !text-[var(--bg-main)] !bg-[var(--brand-color)] !border-[var(--brand-color)] !not-italic"
61
+ >
62
+ {dict.buttons?.subscribe || en.buttons.subscribe}
63
+ </Button>
64
+ </form>
65
+ </section>
66
+ </div>
67
+ )
68
+ }
@@ -0,0 +1,106 @@
1
+ ---
2
+ import { maugliConfig } from '../config/maugli.config';
3
+ import { LANGUAGES } from '../i18n/languages';
4
+ export interface FaqItem {
5
+ question: string;
6
+ answer: string;
7
+ }
8
+ export interface Props {
9
+ summary: string;
10
+ highlights: string[];
11
+ faq: FaqItem[];
12
+ }
13
+ const { summary, highlights, faq } = Astro.props;
14
+ const lang = typeof maugliConfig.defaultLang === 'string' ? maugliConfig.defaultLang : 'en';
15
+ const languageObj = LANGUAGES.find((l) => l.code === lang) || LANGUAGES.find((l) => l.code === 'en');
16
+ const fallbackDict = LANGUAGES.find((l) => l.code === 'en')?.dict || {};
17
+ const dict = languageObj?.dict && Object.keys(languageObj.dict).length > 0 ? languageObj.dict : fallbackDict;
18
+ const summaryFAQCard = (dict as any).summaryFAQCard || (fallbackDict as any).summaryFAQCard || {};
19
+ ---
20
+
21
+ <section
22
+ class="w-full bg-[var(--bg-muted)] rounded-custom p-6 pl-8 sm:p-8 sm:pl-10 mt-0 mb-4 flex flex-col gap-6 sm:gap-10 overflow-hidden"
23
+ aria-label={summaryFAQCard.ariaLabel || 'Саммари и FAQ'}
24
+ >
25
+ <div class="flex flex-col sm:flex-row gap-4 sm:gap-6">
26
+ <div class="flex-shrink-0">
27
+ <h3 class="font-serif text-[18px] font-bold flex items-center">
28
+ <span class="mr-2">
29
+ <svg width="20" height="20" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
30
+ <g clip-path="url(#clip0_3491_12503)">
31
+ <path
32
+ fill-rule="evenodd"
33
+ clip-rule="evenodd"
34
+ d="M12.5 0C9.18479 0 6.00537 1.31696 3.66117 3.66117C1.31696 6.00537 0 9.18479 0 12.5C0 15.8152 1.31696 18.9946 3.66117 21.3388C6.00537 23.683 9.18479 25 12.5 25C15.8152 25 18.9946 23.683 21.3388 21.3388C23.683 18.9946 25 15.8152 25 12.5C25 9.18479 23.683 6.00537 21.3388 3.66117C18.9946 1.31696 15.8152 0 12.5 0ZM8 9H9V10H8V9ZM10 9H18V10H10V9ZM8 11H9V12H8V11ZM10 11H15V12H10V11ZM8 13H9V14H8V13ZM10 13H17V14H10V13ZM8 15H9V16H8V15ZM10 15H19V16H10V15Z"
35
+ fill="var(--brand-color)"></path>
36
+ </g>
37
+ <defs>
38
+ <clipPath id="clip0_3491_12503">
39
+ <rect width="25" height="25" fill="white"></rect>
40
+ </clipPath>
41
+ </defs>
42
+ </svg>
43
+ </span>
44
+ <span class="text-[var(--brand-color)]">{summaryFAQCard.summaryLabel || 'Саммари:'}</span>
45
+ </h3>
46
+ </div>
47
+ <div class="flex-1 min-w-0">
48
+ <div class="font-sans text-[16px] mb-4 text-[var(--text-main)] max-w-[calc(100%-2rem)]">{summary}</div>
49
+ <ul class="list-disc ml-6 font-sans text-[14px] text-[var(--text-heading)] leading-[1.2]">
50
+ {highlights.map((hl) => <li>{hl}</li>)}
51
+ </ul>
52
+ </div>
53
+ </div>
54
+ <div class="flex flex-col sm:flex-row gap-4 sm:gap-6">
55
+ <div class="flex-shrink-0">
56
+ <h3 class="font-serif text-[18px] font-bold flex items-center">
57
+ <span class="mr-2">
58
+ <svg width="20" height="20" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
59
+ <g clip-path="url(#clip0_3491_12505)">
60
+ <path
61
+ fill-rule="evenodd"
62
+ clip-rule="evenodd"
63
+ d="M12.5 0C19.4036 0 25 5.59644 25 12.5C25 19.4036 19.4036 25 12.5 25C5.59644 25 0 19.4036 0 12.5C0 5.59644 5.59644 0 12.5 0ZM12.7549 16.1074C12.5033 16.1075 12.2876 16.2001 12.1074 16.3857C11.9271 16.5715 11.8369 16.7943 11.8369 17.0537C11.837 17.313 11.9272 17.5359 12.1074 17.7217C12.2876 17.9072 12.5034 17.9999 12.7549 18C12.9248 18 13.0779 17.958 13.2139 17.874C13.3533 17.7864 13.4643 17.6701 13.5459 17.5264C13.6308 17.3828 13.6738 17.2253 13.6738 17.0537C13.6738 16.7944 13.5835 16.5715 13.4033 16.3857C13.2230 16.2 13.0066 16.1074 12.7549 16.1074ZM13.041 7C12.4901 7 11.9885 7.11263 11.5361 7.33691C11.0872 7.56126 10.7247 7.88541 10.4492 8.30957C10.1772 8.73371 10.0272 9.24397 10 9.83984H11.2861C11.3133 9.4262 11.4134 9.09086 11.5869 8.83496C11.7603 8.57924 11.976 8.39162 12.2344 8.27246C12.4929 8.15328 12.7621 8.09375 13.041 8.09375C13.3606 8.09378 13.6513 8.16218 13.9131 8.29883C14.175 8.43554 14.3846 8.63042 14.541 8.88281C14.6973 9.13512 14.7754 9.43301 14.7754 9.77637C14.7754 10.0532 14.728 10.3054 14.6328 10.5332C14.541 10.761 14.4147 10.9625 14.2549 11.1377C14.095 11.3095 13.9145 11.4588 13.7139 11.585C13.3806 11.7917 13.0955 12.0184 12.8574 12.2637C12.6193 12.509 12.4349 12.8295 12.3057 13.2256C12.1764 13.6217 12.1084 14.1547 12.1016 14.8242V14.8867H13.3262V14.8242C13.333 14.3895 13.3774 14.0263 13.459 13.7354C13.5406 13.4445 13.6714 13.1937 13.8516 12.9834C14.0318 12.7696 14.2721 12.5668 14.5713 12.374C14.8706 12.1847 15.126 11.9671 15.3369 11.7217C15.5511 11.4764 15.714 11.1976 15.8262 10.8857C15.9418 10.5739 16 10.2253 16 9.83984C16 9.29312 15.8776 8.80552 15.6328 8.37793C15.3913 7.95027 15.0471 7.61354 14.6016 7.36816C14.1595 7.12293 13.6394 7.00003 13.041 7Z"
64
+ fill="var(--brand-color)"></path>
65
+ </g>
66
+ <defs>
67
+ <clipPath id="clip0_3491_12505">
68
+ <rect width="25" height="25" fill="white"></rect>
69
+ </clipPath>
70
+ </defs>
71
+ </svg>
72
+ </span>
73
+ <span class="text-[var(--brand-color)]">{summaryFAQCard.faqLabel || 'FAQ:'}</span>
74
+ </h3>
75
+ </div>
76
+ <div class="flex-1 min-w-0 flex flex-col gap-4">
77
+ {
78
+ faq.map(({ question, answer }) => (
79
+ <div>
80
+ <div class="font-sans text-[16px] text-[var(--text-heading)] mb-1 flex items-center">
81
+ <span class="mr-2 flex-shrink-0">
82
+ <svg width="16" height="16" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
83
+ <g clip-path="url(#clip0_3491_12515)">
84
+ <path
85
+ fill-rule="evenodd"
86
+ clip-rule="evenodd"
87
+ d="M12.5 0C19.4036 0 25 5.59644 25 12.5C25 19.4036 19.4036 25 12.5 25C5.59644 25 0 19.4036 0 12.5C0 5.59644 5.59644 0 12.5 0ZM9.20801 8.80273L14.6641 12.499L9.8291 15.7764L9.20801 16.1963L10.0498 17.4385L16.4209 13.1201C16.6265 12.9806 16.7499 12.7485 16.75 12.5C16.75 12.2824 16.6556 12.0769 16.4941 11.9355L16.4209 11.8789L10.0498 7.56055L9.20801 8.80273Z"
88
+ fill="var(--brand-color)"
89
+ />
90
+ </g>
91
+ <defs>
92
+ <clipPath id="clip0_3491_12515">
93
+ <rect width="25" height="25" fill="white" />
94
+ </clipPath>
95
+ </defs>
96
+ </svg>
97
+ </span>
98
+ {question}
99
+ </div>
100
+ <div class="font-sans text-[14px] text-[var(--text-main)] ml-6">{answer}</div>
101
+ </div>
102
+ ))
103
+ }
104
+ </div>
105
+ </div>
106
+ </section>
@@ -0,0 +1,143 @@
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
+ export interface Heading {
13
+ title: string;
14
+ id: string;
15
+ }
16
+
17
+ export interface Props {
18
+ headings: Heading[];
19
+ title?: string;
20
+ }
21
+
22
+ const currentLang = maugliConfig.defaultLang || 'en';
23
+ const dict = dicts[currentLang] || dicts['en'] || {};
24
+ const fallbackDict = dicts['en'] || {};
25
+ const { headings, title = dict.tableOfContents?.title || fallbackDict.tableOfContents?.title || 'Contents' } = Astro.props;
26
+ ---
27
+
28
+ <div
29
+ class="w-full mb-4 rounded-custom p-6 sm:p-8 flex flex-col gap-3 card-shadow card-blur bg-[var(--bg-muted)]"
30
+ id="table-of-contents"
31
+ aria-label={dict.tableOfContents.ariaLabel || fallbackDict.tableOfContents.ariaLabel || 'Contents'}
32
+ >
33
+ <h3 class="font-serif font-bold text-[18px] leading-[100%] text-[var(--brand-color)] flex-none">
34
+ {title}
35
+ </h3>
36
+
37
+ <div class="flex flex-col gap-2 flex-none">
38
+ {
39
+ headings.map((heading) => (
40
+ <a
41
+ href={`#${heading.id}`}
42
+ class="toc-link font-sans font-normal text-[12px] leading-[15px] text-[var(--text-heading)] hover:text-[var(--brand-color)] transition-all duration-300 ease-in-out hover:translate-x-1 transform"
43
+ data-target={heading.id}
44
+ >
45
+ {heading.title}
46
+ </a>
47
+ ))
48
+ }
49
+ </div>
50
+ </div>
51
+
52
+ <style>
53
+ #table-of-contents {
54
+ max-height: calc(100vh - 4rem);
55
+ overflow-y: auto;
56
+ }
57
+
58
+ .toc-link.active {
59
+ color: var(--brand-color);
60
+ font-weight: 600;
61
+ transform: translateX(4px);
62
+ }
63
+ </style>
64
+
65
+ <script>
66
+ document.addEventListener('astro:page-load', () => {
67
+ const tocLinks = document.querySelectorAll('.toc-link');
68
+ if (!tocLinks.length) return;
69
+
70
+ function getSections() {
71
+ return Array.from(tocLinks)
72
+ .map((link) => {
73
+ const id = (link as HTMLElement).dataset.target;
74
+ return id ? document.getElementById(id) : null;
75
+ })
76
+ .filter(Boolean) as HTMLElement[];
77
+ }
78
+
79
+ function highlightActiveSection() {
80
+ const sections = getSections();
81
+ if (!sections.length) return;
82
+
83
+ const scrollPosition = window.scrollY + 100;
84
+ let activeSection = sections[0];
85
+
86
+ for (const section of sections) {
87
+ if (section && section.offsetTop <= scrollPosition) {
88
+ activeSection = section;
89
+ }
90
+ }
91
+
92
+ tocLinks.forEach((link) => link.classList.remove('active'));
93
+
94
+ if (activeSection) {
95
+ const activeLink = document.querySelector(`.toc-link[data-target="${activeSection.id}"]`);
96
+ if (activeLink) activeLink.classList.add('active');
97
+ }
98
+ }
99
+
100
+ // Обработчики событий
101
+ window.addEventListener('scroll', highlightActiveSection, { passive: true });
102
+ window.addEventListener('resize', highlightActiveSection);
103
+
104
+ // Инициализация
105
+ setTimeout(highlightActiveSection, 100);
106
+ highlightActiveSection();
107
+
108
+ // Обработка кликов по ссылкам
109
+ tocLinks.forEach((link) => {
110
+ link.addEventListener('click', (e) => {
111
+ e.preventDefault();
112
+ const targetId = link.getAttribute('href');
113
+ if (targetId) {
114
+ const el = document.querySelector(targetId);
115
+ if (el) {
116
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' });
117
+ window.history.pushState(null, '', targetId);
118
+ }
119
+ }
120
+ });
121
+ });
122
+ });
123
+ </script>
124
+
125
+ <style>
126
+ /* Добавляем плавный скролл для якорных ссылок */
127
+ :global(html) {
128
+ scroll-behavior: smooth;
129
+ }
130
+
131
+ /* Стили для активной ссылки с более плавной анимацией */
132
+ .toc-link.active {
133
+ color: var(--brand-color) !important;
134
+ font-weight: 500;
135
+ transform: translateX(4px);
136
+ transition: all 0.3s ease-in-out;
137
+ }
138
+
139
+ /* Добавляем офсет для якорных ссылок, чтобы заголовки не скрывались под header */
140
+ :global(h2[id], h3[id]) {
141
+ scroll-margin-top: 80px;
142
+ }
143
+ </style>
@@ -0,0 +1,41 @@
1
+ ---
2
+ import { maugliConfig } from '../config/maugli.config';
3
+ import { LANGUAGES } from '../i18n/languages';
4
+
5
+ export interface Props {
6
+ tag: string;
7
+ href?: string;
8
+ class?: string;
9
+ }
10
+
11
+ const { tag, href, class: className = '' } = Astro.props;
12
+ const lang = typeof maugliConfig.defaultLang === 'string' ? maugliConfig.defaultLang : 'en';
13
+ const languageObj = LANGUAGES.find((l) => l.code === lang) || LANGUAGES.find((l) => l.code === 'en');
14
+ const fallbackDict = LANGUAGES.find((l) => l.code === 'en')?.dict || {};
15
+ const dict = languageObj?.dict && Object.keys(languageObj.dict).length > 0 ? languageObj.dict : fallbackDict;
16
+ const tagPill = (dict as any).tagPill || (fallbackDict as any).tagPill || {};
17
+ const ariaLabel = tagPill.ariaLabel ? tagPill.ariaLabel.replace('{tag}', tag) : `Tag: ${tag}`;
18
+ ---
19
+
20
+ {
21
+ href ? (
22
+ <a
23
+ href={href}
24
+ aria-label={ariaLabel}
25
+ class={`inline-flex items-center justify-center px-3 py-1 bg-[var(--brand-color-20)] border border-[var(--brand-color)] rounded-3xl font-inter font-normal text-sm leading-[15px] text-[var(--brand-color)] whitespace-nowrap no-underline transition-all duration-200 ease-in-out hover:bg-[var(--brand-color-60)] hover:-translate-y-px ${className}`}
26
+ >
27
+ {tag}
28
+ </a>
29
+ ) : (
30
+ <span
31
+ aria-label={ariaLabel}
32
+ class={`inline-flex items-center justify-center px-3 py-1 bg-[var(--brand-color-20)] border border-[var(--brand-color)] rounded-3xl font-inter font-normal text-sm leading-[15px] text-[var(--brand-color)] whitespace-nowrap transition-all duration-200 ease-in-out hover:bg-[var(--brand-color-60)] hover:-translate-y-px ${className}`}
33
+ >
34
+ {tag}
35
+ </span>
36
+ )
37
+ }
38
+
39
+ <style>
40
+ /* Пустой стиль блок оставляем для будущих дополнений если понадобятся */
41
+ </style>
@@ -0,0 +1,42 @@
1
+ ---
2
+ import { maugliConfig } from '../config/maugli.config';
3
+ import { LANGUAGES } from '../i18n/languages';
4
+ export interface Props {
5
+ tags: Array<{ id: string; name: string; count: number }>;
6
+ basePath?: string;
7
+ class?: string;
8
+ }
9
+
10
+ const { tags = [], basePath = '/blog', class: className = '' } = Astro.props;
11
+
12
+ // Определяем куда ведут теги
13
+ const tagsBasePath = basePath === '/projects' ? '/projects/tags' : '/tags';
14
+
15
+ const lang = typeof maugliConfig.defaultLang === 'string' ? maugliConfig.defaultLang : 'en';
16
+ const languageObj = LANGUAGES.find((l) => l.code === lang) || LANGUAGES.find((l) => l.code === 'en');
17
+ const fallbackDict = LANGUAGES.find((l) => l.code === 'en')?.dict || {};
18
+ const dict = languageObj?.dict && Object.keys(languageObj.dict).length > 0 ? languageObj.dict : fallbackDict;
19
+ const tagPills = (dict as any).tagPills || (fallbackDict as any).tagPills || {};
20
+ ---
21
+
22
+ {
23
+ tags.length > 0 && (
24
+ <div class:list={['flex flex-wrap gap-2 py-4 mb-6', className]}>
25
+ {tags.map((tag) => {
26
+ const ariaLabel = tagPills.ariaLabel
27
+ ? tagPills.ariaLabel.replace('{name}', tag.name).replace('{count}', tag.count)
28
+ : `Tag: ${tag.name} (${tag.count})`;
29
+ return (
30
+ <a
31
+ href={`${tagsBasePath}/${tag.id}`}
32
+ aria-label={ariaLabel}
33
+ class="inline-flex items-center justify-center px-4 py-1.5 text-lg leading-[22px] font-inter font-normal whitespace-nowrap no-underline transition-all duration-200 ease-in-out hover:-translate-y-px rounded-3xl border border-[var(--border-main)] text-[var(--text-heading)] hover:bg-[var(--bg-muted)]"
34
+ >
35
+ {tag.name}
36
+ <span style="color: var(--text-heading); opacity: 0.6;"> ({tag.count})</span>
37
+ </a>
38
+ );
39
+ })}
40
+ </div>
41
+ )
42
+ }