@dsbasko/cookbook-engine 0.1.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.

Potentially problematic release.


This version of @dsbasko/cookbook-engine might be problematic. Click here for more details.

Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +232 -0
  3. package/assets/fonts/jetbrains-mono/JetBrainsMono-Bold.woff2 +0 -0
  4. package/assets/fonts/jetbrains-mono/JetBrainsMono-BoldItalic.woff2 +0 -0
  5. package/assets/fonts/jetbrains-mono/JetBrainsMono-Italic.woff2 +0 -0
  6. package/assets/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2 +0 -0
  7. package/assets/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2 +0 -0
  8. package/assets/fonts/jetbrains-mono/JetBrainsMono-SemiBold.woff2 +0 -0
  9. package/package.json +92 -0
  10. package/scripts/check-course-coverage.mts +32 -0
  11. package/scripts/fix-static-image-extensions.mjs +78 -0
  12. package/scripts/generate-readme-toc.mts +32 -0
  13. package/scripts/resolve-course-paths.mjs +28 -0
  14. package/scripts/sync-images.mjs +88 -0
  15. package/src/components/AppShell/AppShell.module.css +40 -0
  16. package/src/components/AppShell/AppShell.tsx +135 -0
  17. package/src/components/AppShell/index.ts +1 -0
  18. package/src/components/Callout/Callout.module.css +68 -0
  19. package/src/components/Callout/Callout.tsx +83 -0
  20. package/src/components/Callout/index.ts +1 -0
  21. package/src/components/CodeBlock/CodeBlock.module.css +68 -0
  22. package/src/components/CodeBlock/CodeBlock.tsx +65 -0
  23. package/src/components/CodeBlock/index.ts +1 -0
  24. package/src/components/GateProvider/GateProvider.tsx +207 -0
  25. package/src/components/GateProvider/index.ts +1 -0
  26. package/src/components/Header/Breadcrumbs.tsx +50 -0
  27. package/src/components/Header/Header.module.css +131 -0
  28. package/src/components/Header/Header.tsx +26 -0
  29. package/src/components/Header/HeaderLessonNav.tsx +118 -0
  30. package/src/components/Header/index.ts +1 -0
  31. package/src/components/HomePage/HomePage.module.css +538 -0
  32. package/src/components/HomePage/HomePage.tsx +295 -0
  33. package/src/components/HomePage/index.ts +1 -0
  34. package/src/components/LessonAwareLink/LessonAwareLink.module.css +12 -0
  35. package/src/components/LessonAwareLink/LessonAwareLink.tsx +86 -0
  36. package/src/components/LessonAwareLink/index.ts +1 -0
  37. package/src/components/LessonLayout/LessonLayout.module.css +35 -0
  38. package/src/components/LessonLayout/LessonLayout.tsx +18 -0
  39. package/src/components/LessonLayout/index.ts +1 -0
  40. package/src/components/LessonLockedInterstitial/LessonLockedInterstitial.module.css +367 -0
  41. package/src/components/LessonLockedInterstitial/LessonLockedInterstitial.tsx +256 -0
  42. package/src/components/LessonLockedInterstitial/index.ts +1 -0
  43. package/src/components/LessonNav/LessonNav.module.css +84 -0
  44. package/src/components/LessonNav/LessonNav.tsx +64 -0
  45. package/src/components/LessonNav/index.ts +1 -0
  46. package/src/components/LessonPageLayout/LessonPageLayout.module.css +118 -0
  47. package/src/components/LessonPageLayout/LessonPageLayout.tsx +46 -0
  48. package/src/components/LessonPageLayout/index.ts +1 -0
  49. package/src/components/LessonSideMeta/LessonSideMeta.module.css +68 -0
  50. package/src/components/LessonSideMeta/LessonSideMeta.tsx +87 -0
  51. package/src/components/LessonSideMeta/index.ts +1 -0
  52. package/src/components/ModulePage/ModulePage.module.css +693 -0
  53. package/src/components/ModulePage/ModulePage.tsx +301 -0
  54. package/src/components/ModulePage/index.ts +1 -0
  55. package/src/components/ProgramDrawer/LockIcon.tsx +19 -0
  56. package/src/components/ProgramDrawer/ProgramDrawer.module.css +563 -0
  57. package/src/components/ProgramDrawer/ProgramDrawer.tsx +481 -0
  58. package/src/components/ProgramDrawer/index.ts +1 -0
  59. package/src/components/ProgressBar/ProgressBar.module.css +46 -0
  60. package/src/components/ProgressBar/ProgressBar.tsx +45 -0
  61. package/src/components/ProgressBar/index.ts +1 -0
  62. package/src/components/ProgressModeProvider/ProgressModeProvider.tsx +87 -0
  63. package/src/components/ProgressModeProvider/index.ts +1 -0
  64. package/src/components/ReadingPrefsProvider/ReadingPrefsProvider.tsx +100 -0
  65. package/src/components/ReadingPrefsProvider/index.ts +1 -0
  66. package/src/components/ReadingProgress/ReadingProgress.module.css +19 -0
  67. package/src/components/ReadingProgress/ReadingProgress.tsx +53 -0
  68. package/src/components/ReadingProgress/index.ts +1 -0
  69. package/src/components/SettingsToggle/SettingsToggle.module.css +888 -0
  70. package/src/components/SettingsToggle/SettingsToggle.tsx +688 -0
  71. package/src/components/SettingsToggle/index.ts +1 -0
  72. package/src/components/Sidebar/Sidebar.module.css +157 -0
  73. package/src/components/Sidebar/Sidebar.tsx +63 -0
  74. package/src/components/Sidebar/icons/GitHubIcon.tsx +17 -0
  75. package/src/components/Sidebar/icons/HomeIcon.tsx +22 -0
  76. package/src/components/Sidebar/icons/LanguageIcon.tsx +24 -0
  77. package/src/components/Sidebar/icons/ProgramIcon.tsx +23 -0
  78. package/src/components/Sidebar/icons/SettingsIcon.tsx +26 -0
  79. package/src/components/Sidebar/icons/ThemeIcon.tsx +22 -0
  80. package/src/components/Sidebar/icons/index.ts +6 -0
  81. package/src/components/Sidebar/index.ts +1 -0
  82. package/src/components/ThemeProvider/ThemeProvider.tsx +68 -0
  83. package/src/components/ThemeProvider/index.ts +1 -0
  84. package/src/components/Toc/Toc.module.css +78 -0
  85. package/src/components/Toc/Toc.tsx +92 -0
  86. package/src/components/Toc/index.ts +1 -0
  87. package/src/components/TranslationBanner/TranslationBanner.module.css +32 -0
  88. package/src/components/TranslationBanner/TranslationBanner.tsx +40 -0
  89. package/src/components/TranslationBanner/index.ts +1 -0
  90. package/src/config.d.mts +12 -0
  91. package/src/config.mjs +110 -0
  92. package/src/index.ts +62 -0
  93. package/src/layout/lang.tsx +44 -0
  94. package/src/layout/root.tsx +223 -0
  95. package/src/lib/course-loader.ts +33 -0
  96. package/src/lib/course.ts +429 -0
  97. package/src/lib/coverage.ts +141 -0
  98. package/src/lib/description.ts +43 -0
  99. package/src/lib/extract-toc.ts +59 -0
  100. package/src/lib/format.ts +55 -0
  101. package/src/lib/frontier-link.ts +37 -0
  102. package/src/lib/gate-init-script.ts +40 -0
  103. package/src/lib/gate-mark-script.ts +324 -0
  104. package/src/lib/i18n.ts +474 -0
  105. package/src/lib/lang.ts +90 -0
  106. package/src/lib/lesson-gate.ts +79 -0
  107. package/src/lib/lesson.ts +66 -0
  108. package/src/lib/markdown-components.tsx +51 -0
  109. package/src/lib/markdown.ts +180 -0
  110. package/src/lib/mdx-plugins/rehype-callout.ts +80 -0
  111. package/src/lib/mdx-plugins/remark-lesson-images.ts +109 -0
  112. package/src/lib/mdx-plugins/remark-link-rewrite.ts +231 -0
  113. package/src/lib/paths.ts +36 -0
  114. package/src/lib/program-drawer.ts +8 -0
  115. package/src/lib/progress-mode.ts +69 -0
  116. package/src/lib/progress.ts +182 -0
  117. package/src/lib/reading-prefs.ts +127 -0
  118. package/src/lib/readme-toc.ts +69 -0
  119. package/src/lib/site-url.ts +33 -0
  120. package/src/lib/sitemap.ts +112 -0
  121. package/src/lib/slug.ts +15 -0
  122. package/src/lib/theme.ts +78 -0
  123. package/src/lib/use-i18n.ts +25 -0
  124. package/src/og/icon.tsx +40 -0
  125. package/src/og/opengraph-image.tsx +126 -0
  126. package/src/pages/home.tsx +66 -0
  127. package/src/pages/lesson.tsx +260 -0
  128. package/src/pages/module.tsx +80 -0
  129. package/src/pages/not-found-lang.tsx +51 -0
  130. package/src/pages/not-found-root.tsx +48 -0
  131. package/src/pages/root.tsx +44 -0
  132. package/src/seo/robots.ts +16 -0
  133. package/src/seo/sitemap.ts +10 -0
  134. package/src/styles/globals.css +139 -0
  135. package/src/styles/markdown.css +265 -0
  136. package/src/styles/reset.css +89 -0
  137. package/src/styles/tokens.css +270 -0
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useGate } from '@/components/GateProvider';
5
+ import { useProgressMode } from '@/components/ProgressModeProvider';
6
+ import { lessonKey, markCompletedAndAdvance } from '@/lib/progress';
7
+ import { useLang, useT } from '@/lib/use-i18n';
8
+ import styles from './LessonNav.module.css';
9
+
10
+ export type LessonNavLink = {
11
+ moduleId: string;
12
+ slug: string;
13
+ title: string;
14
+ };
15
+
16
+ type LessonNavProps = {
17
+ prev: LessonNavLink | null;
18
+ next: LessonNavLink | null;
19
+ currentModuleId: string;
20
+ currentSlug: string;
21
+ };
22
+
23
+ export function LessonNav({ prev, next, currentModuleId, currentSlug }: LessonNavProps) {
24
+ const gate = useGate();
25
+ const { disabled } = useProgressMode();
26
+ const t = useT();
27
+ const lang = useLang();
28
+ const handleNextClick = () => {
29
+ // Free-reading mode: the link only navigates, it must not record progress.
30
+ if (disabled) return;
31
+ // Single entry point: marks completed, advances the sticky furthest
32
+ // pointer, dispatches the change event. Keeps gate state internally
33
+ // consistent so an in-flight read in another tab doesn't see a half-write.
34
+ markCompletedAndAdvance(gate.course, lessonKey(currentModuleId, currentSlug));
35
+ };
36
+
37
+ return (
38
+ <nav className={styles.row} aria-label={t.lessonNavLabel}>
39
+ {prev ? (
40
+ <Link
41
+ href={`/${lang}/${prev.moduleId}/${prev.slug}`}
42
+ className={`${styles.card} ${styles.prev}`}
43
+ >
44
+ <span className={styles.label}>{t.prevLesson}</span>
45
+ <span className={styles.title}>{prev.title}</span>
46
+ </Link>
47
+ ) : (
48
+ <span className={styles.placeholder} aria-hidden="true" />
49
+ )}
50
+ {next ? (
51
+ <Link
52
+ href={`/${lang}/${next.moduleId}/${next.slug}`}
53
+ className={`${styles.card} ${styles.next}`}
54
+ onClick={handleNextClick}
55
+ >
56
+ <span className={styles.label}>{t.nextLesson}</span>
57
+ <span className={styles.title}>{next.title}</span>
58
+ </Link>
59
+ ) : (
60
+ <span className={styles.placeholder} aria-hidden="true" />
61
+ )}
62
+ </nav>
63
+ );
64
+ }
@@ -0,0 +1 @@
1
+ export { LessonNav, type LessonNavLink } from './LessonNav';
@@ -0,0 +1,118 @@
1
+ .page {
2
+ display: grid;
3
+ grid-template-columns: minmax(0, 1fr) 260px;
4
+ gap: 56px;
5
+ align-items: start;
6
+ padding: 56px var(--space-8) 96px;
7
+ max-width: calc(var(--prose-max-width) + 260px + 56px + var(--space-8) * 2);
8
+ width: 100%;
9
+ margin: 0 auto;
10
+ }
11
+
12
+ .page[data-has-side='false'] {
13
+ grid-template-columns: minmax(0, 1fr);
14
+ max-width: calc(var(--prose-max-width) + var(--space-8) * 2);
15
+ }
16
+
17
+ .content {
18
+ min-width: 0;
19
+ }
20
+
21
+ .article {
22
+ min-width: 0;
23
+ max-width: var(--prose-max-width);
24
+ }
25
+
26
+ .hero {
27
+ padding-bottom: var(--space-8);
28
+ margin-bottom: var(--space-8);
29
+ border-bottom: 1px solid var(--bg-stroke);
30
+ }
31
+
32
+ .title {
33
+ font-family: var(--prose-font-active), Georgia, 'Source Serif Pro', serif;
34
+ font-size: calc(var(--prose-font-size) * 4.5);
35
+ line-height: 1;
36
+ letter-spacing: -0.03em;
37
+ font-weight: var(--font-weight-bold);
38
+ margin: 0;
39
+ text-wrap: balance;
40
+ }
41
+
42
+ .prose {
43
+ font-family: var(--prose-font-active), Georgia, 'Source Serif Pro', serif;
44
+ font-size: var(--prose-font-size);
45
+ line-height: 1.7;
46
+ color: var(--content-primary);
47
+ }
48
+
49
+ /* Apply serif sizing to markdown body without rewriting markdown.css. */
50
+ .prose :global(.markdown) {
51
+ font-family: inherit;
52
+ font-size: inherit;
53
+ line-height: inherit;
54
+ }
55
+
56
+ /* Headings inherit the prose font family so the reading-preferences toggle
57
+ * (serif/sans/lora) updates body copy and headings together. */
58
+ .prose :global(.markdown h2) {
59
+ letter-spacing: -0.02em;
60
+ margin-top: 40px;
61
+ margin-bottom: var(--space-5);
62
+ scroll-margin-top: calc(56px + var(--space-4));
63
+ }
64
+
65
+ .prose :global(.markdown p) {
66
+ text-wrap: pretty;
67
+ }
68
+
69
+ .footer {
70
+ margin-top: 56px;
71
+ padding-top: var(--space-8);
72
+ border-top: 1px solid var(--bg-stroke);
73
+ }
74
+
75
+ /* Right sidebar: sticks within its own grid column. */
76
+ .side {
77
+ position: sticky;
78
+ top: calc(56px + var(--space-5));
79
+ display: flex;
80
+ flex-direction: column;
81
+ gap: var(--space-8);
82
+ padding-top: var(--space-2);
83
+ max-height: calc(100dvh - 56px - var(--space-8));
84
+ overflow-y: auto;
85
+ }
86
+
87
+ @media (max-width: 1100px) {
88
+ .page {
89
+ grid-template-columns: minmax(0, 1fr);
90
+ gap: var(--space-8);
91
+ }
92
+
93
+ .side {
94
+ display: none;
95
+ }
96
+
97
+ .title {
98
+ font-size: calc(var(--prose-font-size) * 3.5);
99
+ }
100
+ }
101
+
102
+ @media (max-width: 720px) {
103
+ .page {
104
+ padding: var(--space-6) var(--space-5) var(--space-10);
105
+ }
106
+
107
+ .title {
108
+ font-size: calc(var(--prose-font-size) * 2.4);
109
+ }
110
+
111
+ .prose {
112
+ line-height: 1.65;
113
+ }
114
+
115
+ .prose :global(.markdown h2) {
116
+ margin-top: var(--space-8);
117
+ }
118
+ }
@@ -0,0 +1,46 @@
1
+ import type { ReactNode } from 'react';
2
+ import { getDict } from '@/lib/i18n';
3
+ import { type Lang } from '@/lib/lang';
4
+ import styles from './LessonPageLayout.module.css';
5
+
6
+ type LessonPageLayoutProps = {
7
+ lang: Lang;
8
+ title: ReactNode;
9
+ children: ReactNode;
10
+ footer?: ReactNode;
11
+ tocSlot?: ReactNode;
12
+ sideMetaSlot?: ReactNode;
13
+ };
14
+
15
+ export function LessonPageLayout({
16
+ lang,
17
+ title,
18
+ children,
19
+ footer,
20
+ tocSlot,
21
+ sideMetaSlot,
22
+ }: LessonPageLayoutProps) {
23
+ const hasSide = Boolean(tocSlot || sideMetaSlot);
24
+ const t = getDict(lang);
25
+
26
+ return (
27
+ <div className={styles.page} data-has-side={hasSide ? 'true' : 'false'}>
28
+ <div className={styles.content}>
29
+ <article className={styles.article} data-reading-target="">
30
+ <header className={styles.hero}>
31
+ <h1 className={styles.title}>{title}</h1>
32
+ </header>
33
+ <div className={styles.prose}>{children}</div>
34
+ </article>
35
+ {footer && <footer className={styles.footer}>{footer}</footer>}
36
+ </div>
37
+
38
+ {hasSide && (
39
+ <aside className={styles.side} aria-label={t.lessonInfoLabel}>
40
+ {tocSlot}
41
+ {sideMetaSlot}
42
+ </aside>
43
+ )}
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1 @@
1
+ export { LessonPageLayout } from './LessonPageLayout';
@@ -0,0 +1,68 @@
1
+ .meta {
2
+ border-top: 1px solid var(--bg-stroke);
3
+ padding-top: var(--space-5);
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: var(--space-3);
7
+ font-size: 13px;
8
+ }
9
+
10
+ .row {
11
+ display: flex;
12
+ flex-direction: column;
13
+ gap: 2px;
14
+ }
15
+
16
+ .key {
17
+ font-family: var(--font-mono), ui-monospace, monospace;
18
+ font-size: 12px;
19
+ color: var(--content-tertiary);
20
+ text-transform: uppercase;
21
+ letter-spacing: 0.06em;
22
+ }
23
+
24
+ .value {
25
+ color: var(--content-primary);
26
+ text-decoration: none;
27
+ font-weight: var(--font-weight-medium);
28
+ }
29
+
30
+ a.value:hover {
31
+ color: var(--accent-main);
32
+ }
33
+
34
+ .tags {
35
+ display: flex;
36
+ flex-wrap: wrap;
37
+ gap: 4px;
38
+ }
39
+
40
+ .tag {
41
+ font-family: var(--font-mono), ui-monospace, monospace;
42
+ font-size: 12px;
43
+ padding: 1px 6px;
44
+ background: var(--bg-surface);
45
+ border: 1px solid var(--bg-stroke);
46
+ border-radius: var(--radius-sm);
47
+ color: var(--content-secondary);
48
+ font-weight: var(--font-weight-regular);
49
+ }
50
+
51
+ .markButton {
52
+ composes: btn btnPrimary from '../HomePage/HomePage.module.css';
53
+ margin-top: var(--space-3);
54
+ border-radius: 4px;
55
+ justify-content: center;
56
+ }
57
+
58
+ /* The "Пометить непрочитанным" button is always in the DOM (so SSR markup
59
+ matches every hydration), but only visible when the wrapper carries
60
+ data-completed='true'. The gate-paint inline script stamps that
61
+ attribute synchronously before first paint based on localStorage. */
62
+ .meta [data-show-when-completed] {
63
+ display: none;
64
+ }
65
+
66
+ .meta[data-completed='true'] [data-show-when-completed] {
67
+ display: inline-flex;
68
+ }
@@ -0,0 +1,87 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { useProgressMode } from '@/components/ProgressModeProvider';
5
+ import {
6
+ lessonKey,
7
+ PROGRESS_CHANGE_EVENT,
8
+ unmarkCompleted,
9
+ } from '@/lib/progress';
10
+ import { useLang, useT } from '@/lib/use-i18n';
11
+ import styles from './LessonSideMeta.module.css';
12
+
13
+ type LessonSideMetaProps = {
14
+ moduleId: string;
15
+ moduleTitle: string;
16
+ moduleIndex: number;
17
+ slug: string;
18
+ duration: string;
19
+ tags: string[];
20
+ };
21
+
22
+ export function LessonSideMeta({
23
+ moduleId,
24
+ moduleTitle,
25
+ moduleIndex,
26
+ slug,
27
+ duration,
28
+ tags,
29
+ }: LessonSideMetaProps) {
30
+ const key = lessonKey(moduleId, slug);
31
+ const moduleNum = String(moduleIndex).padStart(2, '0');
32
+ const t = useT();
33
+ const lang = useLang();
34
+ const { disabled } = useProgressMode();
35
+
36
+ const handleUnmark = () => {
37
+ // Defence in depth: in free-reading mode the GateProvider strips
38
+ // `data-completed`, so CSS already hides this button. Guard the write too in
39
+ // case the handler is reached directly — free-reading mode must never touch
40
+ // progress storage.
41
+ if (disabled) return;
42
+ unmarkCompleted(key);
43
+ window.dispatchEvent(new Event(PROGRESS_CHANGE_EVENT));
44
+ };
45
+
46
+ return (
47
+ <div
48
+ className={styles.meta}
49
+ data-lesson-key={key}
50
+ // Gate-mark inline script flips data-completed / data-locked on every
51
+ // [data-lesson-key] before hydration. Without this hint React warns
52
+ // about "extra attributes from the server" on this div.
53
+ suppressHydrationWarning
54
+ >
55
+ <div className={styles.row}>
56
+ <span className={styles.key}>{t.moduleMetaKey}</span>
57
+ <Link href={`/${lang}/${moduleId}`} className={styles.value}>
58
+ {moduleNum} · {moduleTitle}
59
+ </Link>
60
+ </div>
61
+ <div className={styles.row}>
62
+ <span className={styles.key}>{t.readingTimeMetaKey}</span>
63
+ <span className={styles.value}>{duration}</span>
64
+ </div>
65
+ {tags.length > 0 && (
66
+ <div className={styles.row}>
67
+ <span className={styles.key}>{t.tagsMetaKey}</span>
68
+ <span className={styles.tags}>
69
+ {tags.map((tag) => (
70
+ <span key={tag} className={styles.tag}>
71
+ #{tag}
72
+ </span>
73
+ ))}
74
+ </span>
75
+ </div>
76
+ )}
77
+ <button
78
+ type="button"
79
+ className={styles.markButton}
80
+ data-show-when-completed
81
+ onClick={handleUnmark}
82
+ >
83
+ {t.markUnread}
84
+ </button>
85
+ </div>
86
+ );
87
+ }
@@ -0,0 +1 @@
1
+ export { LessonSideMeta } from './LessonSideMeta';