@san-siva/blogkit 1.1.20 → 1.1.21

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 (55) hide show
  1. package/dist/cjs/dynamicComponents/BlogDynamic.js +31 -181
  2. package/dist/cjs/dynamicComponents/BlogDynamic.js.map +1 -1
  3. package/dist/cjs/dynamicComponents/BlogSectionDynamic.js +11 -2
  4. package/dist/cjs/dynamicComponents/BlogSectionDynamic.js.map +1 -1
  5. package/dist/cjs/hooks/useCategoryTitles.js +104 -0
  6. package/dist/cjs/hooks/useCategoryTitles.js.map +1 -0
  7. package/dist/cjs/hooks/useSectionObserver.js +89 -0
  8. package/dist/cjs/hooks/useSectionObserver.js.map +1 -0
  9. package/dist/cjs/index.css +1 -1
  10. package/dist/cjs/index.css.map +1 -1
  11. package/dist/cjs/staticComponents/TocNodeStatic.js +16 -0
  12. package/dist/cjs/staticComponents/TocNodeStatic.js.map +1 -0
  13. package/dist/cjs/styles/Blog.module.scss.js +1 -1
  14. package/dist/cjs/styles/Callout.module.scss.js +1 -1
  15. package/dist/cjs/styles/TocNode.module.scss.js +8 -0
  16. package/dist/cjs/styles/TocNode.module.scss.js.map +1 -0
  17. package/dist/esm/dynamicComponents/BlogDynamic.js +32 -182
  18. package/dist/esm/dynamicComponents/BlogDynamic.js.map +1 -1
  19. package/dist/esm/dynamicComponents/BlogSectionDynamic.js +12 -3
  20. package/dist/esm/dynamicComponents/BlogSectionDynamic.js.map +1 -1
  21. package/dist/esm/hooks/useCategoryTitles.js +102 -0
  22. package/dist/esm/hooks/useCategoryTitles.js.map +1 -0
  23. package/dist/esm/hooks/useSectionObserver.js +87 -0
  24. package/dist/esm/hooks/useSectionObserver.js.map +1 -0
  25. package/dist/esm/index.css +1 -1
  26. package/dist/esm/index.css.map +1 -1
  27. package/dist/esm/staticComponents/TocNodeStatic.js +12 -0
  28. package/dist/esm/staticComponents/TocNodeStatic.js.map +1 -0
  29. package/dist/esm/styles/Blog.module.scss.js +1 -1
  30. package/dist/esm/styles/Callout.module.scss.js +1 -1
  31. package/dist/esm/styles/TocNode.module.scss.js +4 -0
  32. package/dist/esm/styles/TocNode.module.scss.js.map +1 -0
  33. package/dist/types/dynamicComponents/BlogDynamic.d.ts +1 -1
  34. package/dist/types/dynamicComponents/BlogDynamic.d.ts.map +1 -1
  35. package/dist/types/dynamicComponents/BlogSectionDynamic.d.ts.map +1 -1
  36. package/dist/types/hooks/useCategoryTitles.d.ts +22 -0
  37. package/dist/types/hooks/useCategoryTitles.d.ts.map +1 -0
  38. package/dist/types/hooks/useSectionObserver.d.ts +11 -0
  39. package/dist/types/hooks/useSectionObserver.d.ts.map +1 -0
  40. package/dist/types/staticComponents/TocNodeStatic.d.ts +16 -0
  41. package/dist/types/staticComponents/TocNodeStatic.d.ts.map +1 -0
  42. package/package.json +5 -3
  43. package/src/dynamicComponents/BlogDynamic.tsx +42 -253
  44. package/src/dynamicComponents/BlogSectionDynamic.tsx +16 -2
  45. package/src/hooks/useCategoryTitles.ts +148 -0
  46. package/src/hooks/useSectionObserver.ts +102 -0
  47. package/src/staticComponents/TocNodeStatic.tsx +52 -0
  48. package/src/styles/Blog.module.scss +0 -30
  49. package/src/styles/Blog.module.scss.d.ts +0 -4
  50. package/src/styles/BlogLink.module.scss +1 -1
  51. package/src/styles/BlogSection.module.scss +36 -13
  52. package/src/styles/CodeBlock.module.scss +2 -2
  53. package/src/styles/Table.module.scss +1 -1
  54. package/src/styles/TocNode.module.scss +49 -0
  55. package/src/styles/TocNode.module.scss.d.ts +11 -0
@@ -0,0 +1,148 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+
5
+ import type { Dispatch, SetStateAction } from 'react';
6
+
7
+ import type { ForwardedReference } from '../dynamicComponents/BlogDynamic';
8
+
9
+ export interface SectionReferenceValue {
10
+ el: HTMLElement;
11
+ title: string;
12
+ depth: number;
13
+ }
14
+
15
+ export interface CategoryTitleValue extends SectionReferenceValue {
16
+ lastUpdatedAt: number;
17
+ }
18
+
19
+ export type CategoryTitle = Map<string, CategoryTitleValue>;
20
+
21
+ type SectionReference = Map<string, SectionReferenceValue>;
22
+
23
+ interface Options {
24
+ visibleTitle: string | null;
25
+ setVisibleTitle: Dispatch<SetStateAction<string | null>>;
26
+ setShowTOC: Dispatch<SetStateAction<boolean>>;
27
+ }
28
+
29
+ export function useCategoryTitles({
30
+ visibleTitle,
31
+ setVisibleTitle,
32
+ setShowTOC,
33
+ }: Options) {
34
+ const sectionReferences = useRef<SectionReference>(new Map());
35
+ const [categoryTitles, setCategoryTitles] = useState<CategoryTitle>(
36
+ new Map()
37
+ );
38
+ const updateTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
39
+
40
+ const sortByDomPosition = useCallback(
41
+ (
42
+ [, a]: [string, SectionReferenceValue],
43
+ [, b]: [string, SectionReferenceValue]
44
+ ) => {
45
+ const position = a.el.compareDocumentPosition(b.el);
46
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
47
+ return -1;
48
+ } else if (position & Node.DOCUMENT_POSITION_PRECEDING) {
49
+ return 1;
50
+ }
51
+ return 0;
52
+ },
53
+ []
54
+ );
55
+
56
+ const updateCategoryTitles = useCallback(() => {
57
+ const now = Date.now();
58
+ const newCategoryTitles = new Map<string, CategoryTitleValue>();
59
+
60
+ const sectionsArray = Array.from(sectionReferences.current.entries());
61
+ sectionsArray.sort(sortByDomPosition);
62
+
63
+ let firstSectionId: string | null = null;
64
+ for (const [id, { title, el, depth }] of sectionsArray) {
65
+ if (!firstSectionId) {
66
+ firstSectionId = id;
67
+ }
68
+ newCategoryTitles.set(id, {
69
+ el,
70
+ title,
71
+ lastUpdatedAt: now,
72
+ depth,
73
+ });
74
+ }
75
+
76
+ if (newCategoryTitles.size === 0) return;
77
+
78
+ setCategoryTitles(newCategoryTitles);
79
+ setShowTOC(true);
80
+
81
+ if (visibleTitle) return;
82
+ setVisibleTitle(firstSectionId);
83
+ }, [visibleTitle, sortByDomPosition, setShowTOC, setVisibleTitle]);
84
+
85
+ const debounceUpdateCategoryTitles = useCallback(() => {
86
+ if (updateTimerRef.current) {
87
+ clearTimeout(updateTimerRef.current);
88
+ }
89
+ updateTimerRef.current = setTimeout(() => {
90
+ updateCategoryTitles();
91
+ }, 200);
92
+ }, [updateCategoryTitles]);
93
+
94
+ const removeStaleRefs = (ref: HTMLDivElement) => {
95
+ for (const [existingId, { el }] of sectionReferences.current) {
96
+ if (el !== ref) {
97
+ continue;
98
+ }
99
+ sectionReferences.current.delete(existingId);
100
+ break;
101
+ }
102
+ };
103
+
104
+ const handleCategoryTitle = (ref: HTMLDivElement) => {
105
+ const id = ref.dataset.id;
106
+ const title = ref.dataset.title;
107
+ if (!id || !title) return;
108
+
109
+ let depth = 0;
110
+ let parent = ref.parentElement;
111
+ while (parent) {
112
+ if (parent.hasAttribute('data-id')) depth++;
113
+ parent = parent.parentElement;
114
+ }
115
+
116
+ removeStaleRefs(ref);
117
+ sectionReferences.current.set(id, { el: ref, title, depth });
118
+ };
119
+
120
+ const processSection = (element: ForwardedReference) => {
121
+ const { parentRef, childRefs } = element;
122
+ if (parentRef) handleCategoryTitle(parentRef);
123
+ if (Array.isArray(childRefs)) {
124
+ for (const childRef of childRefs) {
125
+ processSection(childRef);
126
+ }
127
+ }
128
+ };
129
+
130
+ const handleSectionReference = useCallback(
131
+ (element: ForwardedReference) => {
132
+ if (!element) return;
133
+ processSection(element);
134
+ debounceUpdateCategoryTitles();
135
+ },
136
+ [debounceUpdateCategoryTitles]
137
+ );
138
+
139
+ useEffect(() => {
140
+ return () => {
141
+ if (updateTimerRef.current) {
142
+ clearTimeout(updateTimerRef.current);
143
+ }
144
+ };
145
+ }, []);
146
+
147
+ return { categoryTitles, handleSectionReference };
148
+ }
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useRef } from 'react';
4
+
5
+ import type { Dispatch, MouseEvent, SetStateAction } from 'react';
6
+
7
+ import lockScrollUpdates from '../utils/lockScrollUpdates';
8
+ import type { CategoryTitle } from './useCategoryTitles';
9
+
10
+ const getSectionFromUrl = () => {
11
+ const url = new URL(window.location.href);
12
+ return url.searchParams.get('section');
13
+ };
14
+
15
+ const updateUrl = (id: string) => {
16
+ const url = new URL(window.location.href);
17
+ url.searchParams.set('section', id);
18
+ window.history.replaceState({}, '', url.toString());
19
+ };
20
+
21
+ const scrollIntoView = (element: HTMLElement) => {
22
+ if (!element) return;
23
+ const top =
24
+ element.getBoundingClientRect().top + document.body.scrollTop - 100;
25
+ document.body.scrollTo({ top, behavior: 'smooth' });
26
+ };
27
+
28
+ interface Options {
29
+ categoryTitles: CategoryTitle;
30
+ setVisibleTitle: Dispatch<SetStateAction<string | null>>;
31
+ }
32
+
33
+ export function useSectionObserver({ categoryTitles, setVisibleTitle }: Options) {
34
+ const isClickScrolling = useRef(false);
35
+ const scrollEndHandlerRef = useRef<(() => void) | null>(null);
36
+ const intersectionObserversRef = useRef<Map<string, IntersectionObserver>>(
37
+ new Map()
38
+ );
39
+
40
+ // Set up IntersectionObservers whenever the set of sections changes
41
+ useEffect(() => {
42
+ for (const [id, { el }] of categoryTitles) {
43
+ const observer = new IntersectionObserver(
44
+ ([entry]) => {
45
+ if (!entry.isIntersecting) return;
46
+ if (isClickScrolling.current) return;
47
+ if (document.body.scrollTop === 0) return;
48
+ setVisibleTitle(visibleId => {
49
+ if (visibleId === id && !entry.isIntersecting) return null;
50
+ if (entry.isIntersecting) return id;
51
+ return visibleId;
52
+ });
53
+ updateUrl(id);
54
+ },
55
+ { threshold: 0.1 }
56
+ );
57
+ intersectionObserversRef.current.set(id, observer);
58
+ observer.observe(el as HTMLElement);
59
+ }
60
+ }, [categoryTitles.size, setVisibleTitle]);
61
+
62
+ // On initial load, scroll to section specified in URL
63
+ useEffect(() => {
64
+ if (categoryTitles.size === 0) return;
65
+ const section = getSectionFromUrl();
66
+ if (!section) return;
67
+ const entry = categoryTitles.get(section);
68
+ if (!entry) return;
69
+ scrollIntoView(entry.el);
70
+ lockScrollUpdates(section, isClickScrolling, scrollEndHandlerRef, setVisibleTitle);
71
+ }, [categoryTitles.size, setVisibleTitle]);
72
+
73
+ // Cleanup observers on unmount
74
+ useEffect(() => {
75
+ return () => {
76
+ if (scrollEndHandlerRef.current) {
77
+ document.body.removeEventListener('scrollend', scrollEndHandlerRef.current);
78
+ }
79
+ for (const observer of intersectionObserversRef.current.values()) {
80
+ observer.disconnect();
81
+ }
82
+ };
83
+ }, []);
84
+
85
+ const handleClickCategoryTitle = useCallback(
86
+ (event: MouseEvent<HTMLParagraphElement>) => {
87
+ const id = event.currentTarget.dataset.id;
88
+ const index = event.currentTarget.dataset.idx;
89
+ if (!id || !index) return;
90
+
91
+ const { el } = categoryTitles.get(id) || {};
92
+ if (!el) return;
93
+
94
+ updateUrl(id);
95
+ scrollIntoView(el);
96
+ lockScrollUpdates(id, isClickScrolling, scrollEndHandlerRef, setVisibleTitle);
97
+ },
98
+ [categoryTitles, setVisibleTitle]
99
+ );
100
+
101
+ return { handleClickCategoryTitle };
102
+ }
@@ -0,0 +1,52 @@
1
+ import type { MouseEvent } from 'react';
2
+
3
+ import styles from '../styles/TocNode.module.scss';
4
+
5
+ export interface TocNode {
6
+ id: string;
7
+ title: string;
8
+ depth: number;
9
+ children: TocNode[];
10
+ }
11
+
12
+ interface TocNodeProperties {
13
+ node: TocNode;
14
+ index: number;
15
+ visibleTitle: string | null;
16
+ onClick: (e: MouseEvent<HTMLParagraphElement>) => void;
17
+ }
18
+
19
+ const TocNode = ({ node, index, visibleTitle, onClick }: TocNodeProperties) => (
20
+ <div>
21
+ <p
22
+ data-idx={index}
23
+ data-id={node.id}
24
+ className={[
25
+ styles['toc-node__title'],
26
+ node.id === visibleTitle ? styles['toc-node__title--active'] : '',
27
+ node.depth === 1 ? styles['toc-node__title--sub'] : '',
28
+ node.depth === 2 ? styles['toc-node__title--sub-sub'] : '',
29
+ ].join(' ')}
30
+ onClick={onClick}
31
+ >
32
+ {node.title}
33
+ </p>
34
+ {node.children.length > 0 && (
35
+ <div
36
+ className={`${styles['toc-node__children']} ${styles[`toc-node__children--${node.depth === 0 ? 'sub' : 'sub-sub'}`]}`}
37
+ >
38
+ {node.children.map((child, i) => (
39
+ <TocNode
40
+ key={child.id}
41
+ node={child}
42
+ index={i}
43
+ visibleTitle={visibleTitle}
44
+ onClick={onClick}
45
+ />
46
+ ))}
47
+ </div>
48
+ )}
49
+ </div>
50
+ );
51
+
52
+ export default TocNode;
@@ -22,36 +22,6 @@
22
22
  padding-top: $top;
23
23
  padding-right: 0;
24
24
  border-radius: styles.$border-radius--1;
25
-
26
- > .category__title {
27
- display: block;
28
- margin-bottom: styles.space(1);
29
- font-size: styles.$font-size--p-small;
30
- font-weight: styles.$font-weight--500;
31
- cursor: pointer;
32
-
33
- animation: fadeInDown 0.3s ease-in-out;
34
- transition: all 0.3s ease-in-out;
35
-
36
- &--active,
37
- &:hover {
38
- text-decoration: underline;
39
- text-decoration-color: styles.$color--primary;
40
- }
41
-
42
- &--active {
43
- color: styles.$color--primary;
44
- }
45
-
46
- &--sub {
47
- font-size: styles.$font-size--small;
48
- font-weight: styles.$font-weight--400;
49
- }
50
-
51
- &:last-child {
52
- margin-bottom: 0;
53
- }
54
- }
55
25
  }
56
26
  }
57
27
 
@@ -3,11 +3,7 @@ declare const styles: {
3
3
  readonly 'blog__content': string;
4
4
  readonly 'blog__sidebar': string;
5
5
  readonly 'margin-bottom--3': string;
6
- readonly 'margin-bottom-imp--2': string;
7
6
  readonly 'category__header': string;
8
- readonly 'category__title': string;
9
- readonly 'category__title--active': string;
10
- readonly 'category__title--sub': string;
11
7
  };
12
8
 
13
9
  export default styles;
@@ -29,7 +29,7 @@
29
29
  > p {
30
30
  font-weight: stylekit.$font-weight--500;
31
31
  font-family: stylekit.$font-family--primary;
32
- font-size: stylekit.$font-size--p-small;
32
+ font-size: stylekit.$font-size--small;
33
33
  margin-right: stylekit.space(0);
34
34
  }
35
35
  }
@@ -18,23 +18,46 @@
18
18
  text-decoration-color: stylekit.$color--primary;
19
19
  }
20
20
  }
21
- }
22
21
 
23
- .blog-section .blog-section > .blog-section__title {
24
- font-size: stylekit.$font-size--h6;
25
- margin-bottom: stylekit.space(2);
26
- }
22
+ // Sub-section (depth 1)
23
+ .blog-section {
24
+ > .blog-section__title {
25
+ font-size: stylekit.$font-size--h6;
26
+ margin-bottom: stylekit.space(2);
27
+ }
27
28
 
28
- .blog-section .blog-section > *:first-child {
29
- margin-top: stylekit.space(4);
30
- }
29
+ > *:first-child {
30
+ margin-top: stylekit.space(4);
31
+ }
31
32
 
32
- .blog-section .blog-section:not(:last-child) {
33
- margin-bottom: stylekit.space(4);
34
- }
33
+ &:not(:last-child) {
34
+ margin-bottom: stylekit.space(4);
35
+ }
35
36
 
36
- .blog-section .blog-section:last-child {
37
- margin-bottom: stylekit.space(8);
37
+ &:last-child {
38
+ margin-bottom: stylekit.space(8);
39
+ }
40
+
41
+ // Sub-sub-section (depth 2)
42
+ .blog-section {
43
+ > .blog-section__title {
44
+ font-size: stylekit.$font-size--p;
45
+ margin-bottom: stylekit.space(1);
46
+ }
47
+
48
+ > *:first-child {
49
+ margin-top: stylekit.space(2);
50
+ }
51
+
52
+ &:not(:last-child) {
53
+ margin-bottom: stylekit.space(2);
54
+ }
55
+
56
+ &:last-child {
57
+ margin-bottom: stylekit.space(4);
58
+ }
59
+ }
60
+ }
38
61
  }
39
62
 
40
63
  .margin-bottom--6 {
@@ -62,7 +62,7 @@ $code-block-background-color: #282a36;
62
62
  > code,
63
63
  > code > * {
64
64
  font-family: stylekit.$font-family--code;
65
- font-size: stylekit.$font-size--p-small;
65
+ font-size: stylekit.$font-size--small;
66
66
  font-weight: unset;
67
67
  font-style: unset;
68
68
  line-height: stylekit.$line-height--normal;
@@ -120,5 +120,5 @@ $code-block-background-color: #282a36;
120
120
  .code-block--static code {
121
121
  color: stylekit.$color--base !important;
122
122
  font-family: stylekit.$font-family--code !important;
123
- font-size: stylekit.$font-size--p-small !important;
123
+ font-size: stylekit.$font-size--small !important;
124
124
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  @mixin cell-base {
4
4
  padding: styles.rem(12) styles.rem(16);
5
- font-size: styles.$font-size--p-small;
5
+ font-size: styles.$font-size--small;
6
6
  font-family: styles.$font-family--code;
7
7
  word-wrap: break-word;
8
8
  overflow-wrap: break-word;
@@ -0,0 +1,49 @@
1
+ @use '@san-siva/stylekit/styles/index.module.scss' as styles;
2
+
3
+ .toc-node {
4
+ &__title {
5
+ display: block;
6
+ margin-bottom: styles.space(1);
7
+ font-size: styles.$font-size--small;
8
+ font-weight: styles.$font-weight--500;
9
+ cursor: pointer;
10
+
11
+ animation: fadeInDown 0.3s ease-in-out;
12
+ transition: all 0.3s ease-in-out;
13
+
14
+ &--active,
15
+ &:hover {
16
+ text-decoration: underline;
17
+ text-decoration-color: styles.$color--primary;
18
+ }
19
+
20
+ &--active {
21
+ color: styles.$color--primary;
22
+ }
23
+
24
+ &--sub {
25
+ font-size: styles.$font-size--very-small;
26
+ }
27
+
28
+ &--sub-sub {
29
+ font-size: styles.$font-size--very-small;
30
+ font-weight: styles.$font-weight--400;
31
+ }
32
+
33
+ &:not(:last-child) {
34
+ margin-bottom: 0;
35
+ }
36
+ }
37
+
38
+ &__children {
39
+ &--sub-sub,
40
+ &--sub {
41
+ padding: styles.space(1) styles.space(2);
42
+ margin: styles.space(1);
43
+ border-left: 1px solid styles.$color--border;
44
+ }
45
+
46
+ &--sub-sub {
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,11 @@
1
+ declare const styles: {
2
+ readonly 'toc-node__title': string;
3
+ readonly 'toc-node__title--active': string;
4
+ readonly 'toc-node__title--sub': string;
5
+ readonly 'toc-node__title--sub-sub': string;
6
+ readonly 'toc-node__children': string;
7
+ readonly 'toc-node__children--sub': string;
8
+ readonly 'toc-node__children--sub-sub': string;
9
+ };
10
+
11
+ export default styles;