@barodoc/theme-docs 1.0.0 → 1.0.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barodoc/theme-docs",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Documentation theme for Barodoc",
5
5
  "type": "module",
6
6
  "exports": {
@@ -30,7 +30,7 @@
30
30
  "lucide-react": "^0.563.0",
31
31
  "mermaid": "^11.12.2",
32
32
  "tailwind-merge": "^3.4.0",
33
- "@barodoc/core": "1.0.0"
33
+ "@barodoc/core": "1.0.1"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "astro": "^5.0.0",
@@ -99,6 +99,46 @@
99
99
  border-radius: 0 !important;
100
100
  background: transparent !important;
101
101
  }
102
+
103
+ /* Inside CodeGroup: no extra border/background, add padding so code has internal spacing */
104
+ .code-group .code-block-wrapper {
105
+ margin: 0;
106
+ border: none;
107
+ border-radius: 0;
108
+ background: transparent;
109
+ /* padding: 1rem 1.25rem; */
110
+ }
111
+
112
+ .code-group .code-block-wrapper pre {
113
+ padding: 0 !important;
114
+ }
115
+
116
+ /* pre not wrapped by CodeCopy (direct child of code-group-content) */
117
+ .code-group .code-group-content > pre {
118
+ padding: 1rem 1.25rem !important;
119
+ }
120
+
121
+ /* CodeItem: one code block per tab; only the code area is visible, no extra wrapper space */
122
+ .code-group .code-item {
123
+ margin: 0;
124
+ padding: 0;
125
+ display: block;
126
+ }
127
+
128
+ .code-group .code-item:not(:first-child) {
129
+ display: none; /* toggled by CodeGroup script */
130
+ }
131
+
132
+ /* .code-group .code-item .code-block-wrapper,
133
+ .code-group .code-item > pre {
134
+ margin: 0 !important;
135
+ border: none !important;
136
+ padding: 0.25rem 0rem !important;
137
+ } */
138
+
139
+ /* .code-group .code-item .code-block-wrapper pre {
140
+ padding: 0 !important;
141
+ } */
102
142
 
103
143
  .code-block-header {
104
144
  display: flex;
@@ -1,5 +1,7 @@
1
1
  ---
2
- // CodeGroup component for tabbed code blocks
2
+ // CodeGroup component for tabbed code blocks.
3
+ // When using CodeItem children, each CodeItem's title is used for the tab label.
4
+ // Optional titles array is only used when not using CodeItem (direct pre children).
3
5
  interface Props {
4
6
  titles?: string[];
5
7
  }
@@ -7,9 +9,9 @@ interface Props {
7
9
  const { titles = [] } = Astro.props;
8
10
  ---
9
11
 
10
- <div class="not-prose my-4 code-group" data-titles={JSON.stringify(titles)}>
11
- <div class="code-group-tabs flex border-b border-[var(--color-border)] bg-[var(--color-bg-secondary)] rounded-t-lg"></div>
12
- <div class="code-group-content">
12
+ <div class="code-group not-prose my-4 rounded-lg border border-[var(--color-border)] overflow-hidden" data-titles={JSON.stringify(titles)}>
13
+ <div class="code-group-tabs flex flex-wrap gap-0 border-b border-[var(--color-border)] bg-[var(--color-bg-tertiary)]"></div>
14
+ <div class="code-group-content bg-[var(--color-bg-secondary)]">
13
15
  <slot />
14
16
  </div>
15
17
  </div>
@@ -19,40 +21,50 @@ const { titles = [] } = Astro.props;
19
21
  document.querySelectorAll('.code-group').forEach((group) => {
20
22
  const tabsContainer = group.querySelector('.code-group-tabs');
21
23
  const contentContainer = group.querySelector('.code-group-content');
22
- const titles = JSON.parse(group.getAttribute('data-titles') || '[]');
23
- const codeBlocks = contentContainer?.querySelectorAll('pre');
24
+ const fallbackTitles = JSON.parse(group.getAttribute('data-titles') || '[]');
24
25
 
25
- if (!tabsContainer || !contentContainer || !codeBlocks?.length) return;
26
+ const codeItems = contentContainer?.querySelectorAll(':scope > .code-item');
27
+ const directPres = contentContainer?.querySelectorAll(':scope > pre');
26
28
 
27
- // Create tabs
28
- codeBlocks.forEach((block, index) => {
29
+ // When CodeItem children exist: use them for tabs (titles from data-title), one tab per .code-item
30
+ const useCodeItems = (codeItems?.length ?? 0) > 0;
31
+ const tabPanels = useCodeItems
32
+ ? Array.from(codeItems!)
33
+ : Array.from(directPres || []);
34
+ const titles = useCodeItems && codeItems?.length
35
+ ? Array.from(codeItems!).map((el) => (el as HTMLElement).getAttribute('data-title') ?? '')
36
+ : fallbackTitles;
37
+ const numTabs = tabPanels.length;
38
+
39
+ if (!tabsContainer || !contentContainer || numTabs === 0) return;
40
+
41
+ // Create tabs from titles; show/hide tabPanels
42
+ tabPanels.forEach((_panel, index) => {
29
43
  const tab = document.createElement('button');
30
- tab.className = 'px-4 py-2 text-sm font-medium transition-colors text-[var(--color-text-secondary)] hover:text-[var(--color-text)]';
31
- tab.textContent = titles[index] || `Tab ${index + 1}`;
44
+ tab.className = 'shrink-0 px-4 py-2 text-sm font-medium transition-colors whitespace-nowrap text-[var(--color-text-muted)] hover:text-[var(--color-text)]';
45
+ tab.textContent = titles[index]?.trim() || `Tab ${index + 1}`;
46
+ const panel = tabPanels[index] as HTMLElement;
32
47
  tab.addEventListener('click', () => {
33
- // Update tabs
34
48
  tabsContainer.querySelectorAll('button').forEach((t, i) => {
35
49
  if (i === index) {
36
- t.classList.add('border-b-2', 'border-primary-600', 'text-primary-600');
37
- t.classList.remove('text-[var(--color-text-secondary)]');
50
+ t.classList.add('border-b-2', 'border-primary-500', 'text-[var(--color-text)]', '-mb-px');
51
+ t.classList.remove('text-[var(--color-text-muted)]');
38
52
  } else {
39
- t.classList.remove('border-b-2', 'border-primary-600', 'text-primary-600');
40
- t.classList.add('text-[var(--color-text-secondary)]');
53
+ t.classList.remove('border-b-2', 'border-primary-500', 'text-[var(--color-text)]', '-mb-px');
54
+ t.classList.add('text-[var(--color-text-muted)]');
41
55
  }
42
56
  });
43
- // Update content
44
- codeBlocks.forEach((b, i) => {
45
- (b as HTMLElement).style.display = i === index ? 'block' : 'none';
57
+ tabPanels.forEach((p, i) => {
58
+ (p as HTMLElement).style.display = i === index ? 'block' : 'none';
46
59
  });
47
60
  });
48
61
  tabsContainer.appendChild(tab);
49
62
 
50
- // Hide all but first
51
63
  if (index === 0) {
52
- tab.classList.add('border-b-2', 'border-primary-600', 'text-primary-600');
53
- tab.classList.remove('text-[var(--color-text-secondary)]');
64
+ tab.classList.add('border-b-2', 'border-primary-500', 'text-[var(--color-text)]', '-mb-px');
65
+ tab.classList.remove('text-[var(--color-text-muted)]');
54
66
  } else {
55
- (block as HTMLElement).style.display = 'none';
67
+ panel.style.display = 'none';
56
68
  }
57
69
  });
58
70
  });
@@ -5,25 +5,91 @@ interface CodeGroupProps {
5
5
  titles?: string[];
6
6
  }
7
7
 
8
+ function isCodeBlockLike(el: React.ReactElement): boolean {
9
+ if (el.type === "pre") return true;
10
+ const className =
11
+ typeof el.props?.className === "string" ? el.props.className : "";
12
+ if (
13
+ className.includes("language-") ||
14
+ className.includes("astro-code") ||
15
+ className.includes("code-block")
16
+ )
17
+ return true;
18
+ return false;
19
+ }
20
+
21
+ function collectCodeBlocks(node: React.ReactNode): React.ReactElement[] {
22
+ const result: React.ReactElement[] = [];
23
+ React.Children.forEach(node, (child) => {
24
+ if (!React.isValidElement(child)) return;
25
+ if (child.type === React.Fragment && child.props?.children != null) {
26
+ result.push(...collectCodeBlocks(child.props.children));
27
+ return;
28
+ }
29
+ if (child.type === "pre" || isCodeBlockLike(child)) {
30
+ result.push(child);
31
+ return;
32
+ }
33
+ if (child.props?.children != null) {
34
+ result.push(...collectCodeBlocks(child.props.children));
35
+ }
36
+ });
37
+ return result;
38
+ }
39
+
8
40
  export function CodeGroup({ children, titles = [] }: CodeGroupProps) {
9
41
  const [activeIndex, setActiveIndex] = React.useState(0);
10
-
11
- // Extract code blocks from children
12
- const codeBlocks = React.Children.toArray(children);
13
-
42
+
43
+ const codeBlocks = React.useMemo(() => {
44
+ let blocks = collectCodeBlocks(children);
45
+ if (blocks.length > 0) return blocks;
46
+ const direct = React.Children.toArray(children).filter(
47
+ (c): c is React.ReactElement => React.isValidElement(c) && c.type != null
48
+ );
49
+ if (direct.length > 1) return direct;
50
+ if (
51
+ direct.length === 1 &&
52
+ direct[0].props?.children != null
53
+ ) {
54
+ const inner = React.Children.toArray(direct[0].props.children).filter(
55
+ (c): c is React.ReactElement =>
56
+ React.isValidElement(c) && c.type != null
57
+ );
58
+ if (inner.length > 0) return inner;
59
+ }
60
+ return direct;
61
+ }, [children]);
62
+
63
+ const tabTitles =
64
+ titles.length >= codeBlocks.length
65
+ ? titles.slice(0, codeBlocks.length)
66
+ : [
67
+ ...titles,
68
+ ...codeBlocks
69
+ .slice(titles.length)
70
+ .map((_, i) => `Tab ${titles.length + i + 1}`),
71
+ ];
72
+
73
+ if (codeBlocks.length === 0) {
74
+ return (
75
+ <div className="code-group not-prose my-4 rounded-lg border border-[var(--color-border)] overflow-hidden p-4 text-[var(--color-text-muted)] text-sm">
76
+ No code blocks found inside CodeGroup.
77
+ </div>
78
+ );
79
+ }
80
+
14
81
  return (
15
- <div className="not-prose my-4 rounded-lg border border-[var(--color-border)] overflow-hidden">
82
+ <div className="code-group not-prose my-4 rounded-lg border border-[var(--color-border)] overflow-hidden">
16
83
  {/* Tabs */}
17
- <div className="flex bg-[var(--color-bg-tertiary)] border-b border-[var(--color-border)]">
18
- {codeBlocks.map((_, index) => {
84
+ <div className="flex flex-wrap gap-0 bg-[var(--color-bg-tertiary)] border-b border-[var(--color-border)]">
85
+ {tabTitles.map((title, index) => {
19
86
  const isActive = activeIndex === index;
20
- const title = titles[index] || `Tab ${index + 1}`;
21
87
  return (
22
88
  <button
23
89
  key={index}
24
90
  type="button"
25
91
  onClick={() => setActiveIndex(index)}
26
- className={`px-4 py-2 text-sm font-medium transition-colors ${
92
+ className={`shrink-0 px-4 py-2 text-sm font-medium transition-colors whitespace-nowrap ${
27
93
  isActive
28
94
  ? "bg-[var(--color-bg-secondary)] text-[var(--color-text)] border-b-2 border-primary-500 -mb-px"
29
95
  : "text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
@@ -34,7 +100,7 @@ export function CodeGroup({ children, titles = [] }: CodeGroupProps) {
34
100
  );
35
101
  })}
36
102
  </div>
37
-
103
+
38
104
  {/* Content */}
39
105
  <div className="bg-[var(--color-bg-secondary)]">
40
106
  {codeBlocks.map((block, index) => (
@@ -0,0 +1,13 @@
1
+ ---
2
+ // Wraps a single code block inside CodeGroup. Use one CodeItem per tab.
3
+ // Put one fenced code block (```lang ... ```) inside; MDX/Shiki handle syntax highlighting.
4
+ interface Props {
5
+ title?: string;
6
+ }
7
+
8
+ const { title = "" } = Astro.props;
9
+ ---
10
+
11
+ <div class="code-item" data-title={title}>
12
+ <slot />
13
+ </div>
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { AstroIntegration } from "astro";
2
- import type { ThemeExport } from "@barodoc/core";
2
+ import type { ThemeExport, ResolvedBarodocConfig } from "@barodoc/core";
3
3
  import mdx from "@astrojs/mdx";
4
4
  import react from "@astrojs/react";
5
5
  import tailwindcss from "@tailwindcss/vite";
@@ -8,7 +8,60 @@ export interface DocsThemeOptions {
8
8
  customCss?: string[];
9
9
  }
10
10
 
11
- function createThemeIntegration(options?: DocsThemeOptions): AstroIntegration {
11
+ /** HAST node with optional children and properties */
12
+ interface HastNode {
13
+ type: string;
14
+ children?: HastNode[];
15
+ properties?: Record<string, unknown>;
16
+ tagName?: string;
17
+ value?: string;
18
+ }
19
+
20
+ function getTextContent(node: HastNode): string {
21
+ if (node.type === "text") return node.value ?? "";
22
+ if (node.children?.length) return node.children.map(getTextContent).join("");
23
+ return "";
24
+ }
25
+
26
+ function isEmptyLine(node: HastNode): boolean {
27
+ if (node.type !== "element" || node.tagName !== "span") return false;
28
+ const cls = node.properties?.className;
29
+ const isLine =
30
+ Array.isArray(cls) && cls.some((c) => c === "line" || (typeof c === "string" && c.includes("line")));
31
+ if (!isLine) return false;
32
+ return /^\s*$/.test(getTextContent(node));
33
+ }
34
+
35
+ /** Removes leading and trailing empty span.line so only the code area is visible. */
36
+ function createTrimEmptyLinesTransformer() {
37
+ return {
38
+ name: "barodoc-trim-empty-lines",
39
+ code(node: HastNode) {
40
+ const lines = node.children;
41
+ if (!lines?.length) return;
42
+ let start = 0;
43
+ let end = lines.length;
44
+ while (start < end && isEmptyLine(lines[start])) start++;
45
+ while (end > start && isEmptyLine(lines[end - 1])) end--;
46
+ node.children = lines.slice(start, end);
47
+ },
48
+ };
49
+ }
50
+
51
+ function createLineNumbersTransformer() {
52
+ return {
53
+ name: "barodoc-line-numbers",
54
+ pre(node: { properties?: Record<string, unknown> }) {
55
+ (this as { addClassToHast: (node: unknown, cls: string) => void }).addClassToHast(node, "line-numbers");
56
+ },
57
+ };
58
+ }
59
+
60
+ function createThemeIntegration(
61
+ config: ResolvedBarodocConfig,
62
+ options?: DocsThemeOptions
63
+ ): AstroIntegration {
64
+ const lineNumbers = config?.lineNumbers === true;
12
65
  return {
13
66
  name: "@barodoc/theme-docs",
14
67
  hooks: {
@@ -41,6 +94,10 @@ function createThemeIntegration(options?: DocsThemeOptions): AstroIntegration {
41
94
  light: "github-light",
42
95
  dark: "github-dark",
43
96
  },
97
+ transformers: [
98
+ createTrimEmptyLinesTransformer(),
99
+ ...(lineNumbers ? [createLineNumbersTransformer()] : []),
100
+ ],
44
101
  },
45
102
  },
46
103
  });
@@ -54,7 +111,7 @@ function createThemeIntegration(options?: DocsThemeOptions): AstroIntegration {
54
111
  export default function docsTheme(options?: DocsThemeOptions): ThemeExport {
55
112
  return {
56
113
  name: "@barodoc/theme-docs",
57
- integration: () => createThemeIntegration(options),
114
+ integration: (config) => createThemeIntegration(config, options),
58
115
  styles: options?.customCss || [],
59
116
  };
60
117
  }
@@ -77,29 +77,29 @@
77
77
  }
78
78
  }
79
79
 
80
- /* Prose styling for MDX content - Mintlify style */
80
+ /* Prose styling for MDX content - */
81
81
  .prose {
82
82
  --tw-prose-body: var(--color-text);
83
83
  --tw-prose-headings: var(--color-text);
84
84
  --tw-prose-links: var(--color-primary-600);
85
85
  --tw-prose-code: var(--color-text);
86
86
  --tw-prose-pre-bg: var(--color-bg-secondary);
87
- font-size: 0.875rem !important;
88
- line-height: 1.6 !important;
87
+ /* font-size: 0.875rem !important; */
88
+ /* line-height: 1.6 !important; */
89
89
  }
90
90
 
91
91
  .dark .prose {
92
92
  --tw-prose-links: var(--color-primary-400);
93
93
  }
94
94
 
95
- /* Headings - Mintlify style */
95
+ /* Headings - */
96
96
  .prose :where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
97
97
  font-size: 1.75rem !important;
98
98
  font-weight: 700 !important;
99
99
  letter-spacing: -0.025em !important;
100
100
  margin-top: 0 !important;
101
101
  margin-bottom: 0.375rem !important;
102
- line-height: 1.25 !important;
102
+ /* line-height: 1.25 !important; */
103
103
  }
104
104
 
105
105
  .prose :where(h2):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
@@ -143,14 +143,14 @@
143
143
  text-underline-offset: 2px !important;
144
144
  }
145
145
 
146
- /* Code block styling - Mintlify style */
146
+ /* Code block styling - */
147
147
  .prose :where(pre):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
148
148
  background-color: var(--color-bg) !important;
149
149
  border: 1px solid var(--color-border) !important;
150
150
  border-radius: 0.5rem !important;
151
151
  padding: 0.75rem 1rem !important;
152
152
  font-size: 0.8125rem !important;
153
- line-height: 1.5 !important;
153
+ line-height: 1.45 !important;
154
154
  overflow-x: auto !important;
155
155
  margin: 0.75rem 0 !important;
156
156
  }
@@ -185,7 +185,7 @@
185
185
  color: var(--color-text-muted) !important;
186
186
  }
187
187
 
188
- /* Blockquotes - Mintlify style */
188
+ /* Blockquotes - */
189
189
  .prose :where(blockquote):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
190
190
  border-left: 3px solid var(--color-primary-500) !important;
191
191
  background-color: var(--color-bg-secondary) !important;
@@ -241,6 +241,20 @@
241
241
  .astro-code {
242
242
  background-color: var(--color-bg) !important;
243
243
  border-radius: 0.5rem;
244
+ line-height: 1.45;
245
+ }
246
+
247
+ .astro-code code {
248
+ display: flex;
249
+ flex-direction: column;
250
+ }
251
+
252
+ /* Tighter line spacing: Shiki outputs each line as span.line */
253
+ .astro-code span.line {
254
+ display: block;
255
+ line-height: 1.45;
256
+ margin: 0;
257
+ padding: 0;
244
258
  }
245
259
 
246
260
  .dark .astro-code,
@@ -249,6 +263,28 @@
249
263
  background-color: var(--shiki-dark-bg) !important;
250
264
  }
251
265
 
266
+ /* Line numbers: applied when barodoc.config.json has "lineNumbers": true */
267
+ .astro-code.line-numbers {
268
+ counter-reset: line;
269
+ }
270
+
271
+ .astro-code.line-numbers span.line {
272
+ display: block;
273
+ counter-increment: line;
274
+ }
275
+
276
+ .astro-code.line-numbers span.line::before {
277
+ content: counter(line);
278
+ display: inline-block;
279
+ min-width: 2.5rem;
280
+ margin-right: 1rem;
281
+ padding-right: 0.5rem;
282
+ text-align: right;
283
+ color: var(--color-text-muted);
284
+ user-select: none;
285
+ border-right: 1px solid var(--color-border);
286
+ }
287
+
252
288
  /* Pagefind search styling */
253
289
  .pagefind-ui {
254
290
  --pagefind-ui-scale: 1;