boltdocs 1.5.0 → 1.6.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.
@@ -0,0 +1,229 @@
1
+ import React, { Children, isValidElement, useState } from "react";
2
+ import {
3
+ Folder,
4
+ FileText,
5
+ File,
6
+ FileCode,
7
+ FileImage,
8
+ ChevronRight,
9
+ } from "lucide-react";
10
+
11
+ export interface FileTreeProps {
12
+ children: React.ReactNode;
13
+ }
14
+
15
+ function getTextContent(node: React.ReactNode): string {
16
+ if (typeof node === "string") return node;
17
+ if (typeof node === "number") return node.toString();
18
+ if (Array.isArray(node)) return node.map(getTextContent).join("");
19
+ if (isValidElement(node)) {
20
+ return getTextContent((node.props as any).children);
21
+ }
22
+ return "";
23
+ }
24
+
25
+ function getFileIcon(filename: string) {
26
+ const name = filename.toLowerCase();
27
+
28
+ if (
29
+ name.endsWith(".ts") ||
30
+ name.endsWith(".tsx") ||
31
+ name.endsWith(".js") ||
32
+ name.endsWith(".jsx") ||
33
+ name.endsWith(".json") ||
34
+ name.endsWith(".mjs") ||
35
+ name.endsWith(".cjs") ||
36
+ name.endsWith(".astro") ||
37
+ name.endsWith(".vue") ||
38
+ name.endsWith(".svelte")
39
+ ) {
40
+ return (
41
+ <FileCode size={16} strokeWidth={2} className="ld-file-tree__icon-file" />
42
+ );
43
+ }
44
+
45
+ if (name.endsWith(".md") || name.endsWith(".mdx") || name.endsWith(".txt")) {
46
+ return (
47
+ <FileText size={16} strokeWidth={2} className="ld-file-tree__icon-file" />
48
+ );
49
+ }
50
+
51
+ if (
52
+ name.endsWith(".png") ||
53
+ name.endsWith(".jpg") ||
54
+ name.endsWith(".jpeg") ||
55
+ name.endsWith(".svg") ||
56
+ name.endsWith(".gif")
57
+ ) {
58
+ return (
59
+ <FileImage
60
+ size={16}
61
+ strokeWidth={2}
62
+ className="ld-file-tree__icon-file"
63
+ />
64
+ );
65
+ }
66
+
67
+ return <File size={16} strokeWidth={2} className="ld-file-tree__icon-file" />;
68
+ }
69
+
70
+ // Helper to reliably check for ul and li elements, including MDX wrappers
71
+ function isListElement(node: any, tag: "ul" | "li"): boolean {
72
+ if (typeof node.type === "string") {
73
+ return node.type === tag;
74
+ }
75
+ if (typeof node.type === "function") {
76
+ return node.type.name === tag || node.type.name?.toLowerCase() === tag;
77
+ }
78
+ // MDX specific wrapper detection
79
+ if (node.props && node.props.originalType === tag) {
80
+ return true;
81
+ }
82
+ if (node.props && node.props.mdxType === tag) {
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+
88
+ function FolderNode({
89
+ labelText,
90
+ nestedNodes,
91
+ depth,
92
+ }: {
93
+ labelText: string;
94
+ nestedNodes: React.ReactNode[];
95
+ depth: number;
96
+ }) {
97
+ const [isOpen, setIsOpen] = useState(true);
98
+
99
+ return (
100
+ <li className="ld-file-tree__item">
101
+ <div
102
+ className="ld-file-tree__label ld-file-tree__label--folder"
103
+ onClick={() => setIsOpen(!isOpen)}
104
+ style={{ cursor: "pointer" }}
105
+ >
106
+ <span className="ld-file-tree__icon ld-file-tree__icon--chevron">
107
+ <ChevronRight
108
+ size={14}
109
+ className={`ld-file-tree__chevron ${isOpen ? "ld-file-tree__chevron--open" : ""}`}
110
+ strokeWidth={3}
111
+ />
112
+ </span>
113
+ <span className="ld-file-tree__icon">
114
+ <Folder
115
+ size={16}
116
+ strokeWidth={2}
117
+ className="ld-file-tree__icon-folder"
118
+ fill="currentColor"
119
+ fillOpacity={0.15}
120
+ />
121
+ </span>
122
+ <span className="ld-file-tree__name">{labelText}</span>
123
+ </div>
124
+ {isOpen && nestedNodes.length > 0 && (
125
+ <div className="ld-file-tree__nested">
126
+ {nestedNodes.map((child, index) => (
127
+ <React.Fragment key={index}>
128
+ {parseNode(child, depth)}
129
+ </React.Fragment>
130
+ ))}
131
+ </div>
132
+ )}
133
+ </li>
134
+ );
135
+ }
136
+
137
+ function parseNode(node: React.ReactNode, depth: number = 0): React.ReactNode {
138
+ if (!isValidElement(node)) {
139
+ return node;
140
+ }
141
+
142
+ if (isListElement(node, "ul")) {
143
+ return (
144
+ <ul
145
+ className={`ld-file-tree__list ${depth === 0 ? "ld-file-tree__list--root" : ""}`}
146
+ >
147
+ {Children.map((node.props as any).children, (child, index) => (
148
+ <React.Fragment key={index}>
149
+ {parseNode(child, depth + 1)}
150
+ </React.Fragment>
151
+ ))}
152
+ </ul>
153
+ );
154
+ }
155
+
156
+ if (isListElement(node, "li")) {
157
+ const children = Children.toArray((node.props as any).children);
158
+
159
+ // Find nested list indicating a directory
160
+ const nestedListIndex = children.findIndex(
161
+ (child) => isValidElement(child) && isListElement(child, "ul"),
162
+ );
163
+ const hasNested = nestedListIndex !== -1;
164
+
165
+ // Separate text label from nested items
166
+ const labelNodes = hasNested
167
+ ? children.slice(0, nestedListIndex)
168
+ : children;
169
+ const nestedNodes = hasNested ? children.slice(nestedListIndex) : [];
170
+
171
+ const rawLabelContent = getTextContent(labelNodes).trim();
172
+ const isExplicitDir = rawLabelContent.endsWith("/");
173
+ const labelText = isExplicitDir
174
+ ? rawLabelContent.slice(0, -1)
175
+ : rawLabelContent;
176
+
177
+ const isFolder = hasNested || isExplicitDir;
178
+
179
+ if (isFolder) {
180
+ return (
181
+ <FolderNode
182
+ labelText={labelText}
183
+ nestedNodes={nestedNodes}
184
+ depth={depth}
185
+ />
186
+ );
187
+ }
188
+
189
+ return (
190
+ <li className="ld-file-tree__item">
191
+ <div className="ld-file-tree__label ld-file-tree__label--file">
192
+ <span className="ld-file-tree__icon ld-file-tree__icon--spacer"></span>
193
+ <span className="ld-file-tree__icon">{getFileIcon(labelText)}</span>
194
+ <span className="ld-file-tree__name">{labelText}</span>
195
+ </div>
196
+ </li>
197
+ );
198
+ }
199
+
200
+ // If node is e.g. a paragraph injected by MDX wrapping the list
201
+ if ((node.props as any).children) {
202
+ return Children.map((node.props as any).children, (child, index) => (
203
+ <React.Fragment key={index}>{parseNode(child, depth)}</React.Fragment>
204
+ ));
205
+ }
206
+
207
+ return node;
208
+ }
209
+
210
+ /**
211
+ * FileTree component displays a customized, styled tree structure for markdown lists.
212
+ *
213
+ * ```mdx
214
+ * <FileTree>
215
+ * - src/
216
+ * - index.ts
217
+ * - components/
218
+ * - Button.tsx
219
+ * - package.json
220
+ * </FileTree>
221
+ * ```
222
+ */
223
+ export function FileTree({ children }: FileTreeProps) {
224
+ return (
225
+ <div className="ld-file-tree" dir="ltr">
226
+ {Children.map(children, (child) => parseNode(child, 0))}
227
+ </div>
228
+ );
229
+ }
@@ -4,7 +4,6 @@ import { NPM } from "../../icons/npm";
4
4
  import { Pnpm } from "../../icons/pnpm";
5
5
  import { Bun } from "../../icons/bun";
6
6
  import { Deno } from "../../icons/deno";
7
- import { Yarn } from "../../icons/yarn";
8
7
 
9
8
  /* ─── Tab (individual panel) ──────────────────────────────── */
10
9
  export interface TabProps {
@@ -43,9 +42,8 @@ export interface TabsProps {
43
42
 
44
43
  const getIconForLabel = (label: string) => {
45
44
  const l = label.toLowerCase();
46
- if (l.includes("npm")) return <NPM />;
47
45
  if (l.includes("pnpm")) return <Pnpm />;
48
- if (l.includes("yarn")) return <Yarn />;
46
+ if (l.includes("npm")) return <NPM />;
49
47
  if (l.includes("bun")) return <Bun />;
50
48
  if (l.includes("deno")) return <Deno />;
51
49
  return null;
@@ -58,7 +56,6 @@ const getIconForLabel = (label: string) => {
58
56
  * <Tabs>
59
57
  * <Tab label="npm">npm install boltdocs</Tab>
60
58
  * <Tab label="pnpm">pnpm add boltdocs</Tab>
61
- * <Tab label="yarn">yarn add boltdocs</Tab>
62
59
  * </Tabs>
63
60
  * ```
64
61
  */
@@ -16,3 +16,6 @@ export type { AdmonitionProps } from "./Admonition";
16
16
 
17
17
  export { List } from "./List";
18
18
  export type { ListProps } from "./List";
19
+
20
+ export { FileTree } from "./FileTree";
21
+ export type { FileTreeProps } from "./FileTree";
@@ -430,3 +430,112 @@
430
430
  .ld-list--arrow .ld-list__icon {
431
431
  color: var(--ld-color-primary);
432
432
  }
433
+
434
+ /* ─── FileTree ────────────────────────────────────────────── */
435
+ .ld-file-tree {
436
+ margin: 1.5rem 0;
437
+ padding: 1rem;
438
+ border-radius: var(--ld-radius-lg);
439
+ border: 1px solid var(--ld-border-subtle);
440
+ background: var(--ld-bg-soft);
441
+ font-family: var(--ld-font-mono);
442
+ font-size: 0.875rem;
443
+ overflow-x: auto;
444
+ }
445
+
446
+ .ld-file-tree__list {
447
+ list-style: none !important;
448
+ margin: 0 !important;
449
+ padding: 0;
450
+ position: relative;
451
+ }
452
+
453
+ .ld-file-tree__list:not(.ld-file-tree__list--root) {
454
+ padding-left: 1.25rem;
455
+ margin-top: 0.25rem !important;
456
+ position: relative;
457
+ }
458
+
459
+ /* Vertical line for nested folders */
460
+ .ld-file-tree__list:not(.ld-file-tree__list--root)::before {
461
+ content: "";
462
+ position: absolute;
463
+ top: 0;
464
+ bottom: 0;
465
+ left: 0.45rem; /* align with folder icon */
466
+ width: 1px;
467
+ background-color: var(--ld-border-subtle);
468
+ z-index: 0;
469
+ }
470
+
471
+ .ld-file-tree__item {
472
+ position: relative;
473
+ margin: 0.2rem 0 !important;
474
+ padding: 0 !important;
475
+ display: block !important;
476
+ }
477
+
478
+ .ld-file-tree__label {
479
+ display: inline-flex;
480
+ align-items: center;
481
+ gap: 0.5rem;
482
+ color: var(--ld-text-muted);
483
+ user-select: none;
484
+ border-radius: var(--ld-radius-sm);
485
+ padding: 0.25rem 0.6rem 0.25rem 0.4rem;
486
+ transition: all 0.2s ease;
487
+ position: relative;
488
+ z-index: 1;
489
+ }
490
+
491
+ .ld-file-tree__label:hover {
492
+ background: var(--ld-bg-mute);
493
+ color: var(--ld-text-main);
494
+ }
495
+
496
+ .ld-file-tree__label--folder {
497
+ color: var(--ld-text-main);
498
+ font-weight: 500;
499
+ }
500
+
501
+ .ld-file-tree__icon {
502
+ display: inline-flex;
503
+ align-items: center;
504
+ justify-content: center;
505
+ opacity: 0.8;
506
+ flex-shrink: 0;
507
+ }
508
+
509
+ .ld-file-tree__icon-folder {
510
+ color: var(--ld-color-primary);
511
+ }
512
+
513
+ .ld-file-tree__name {
514
+ white-space: nowrap;
515
+ }
516
+
517
+ .ld-file-tree__icon--chevron {
518
+ width: 14px;
519
+ height: 14px;
520
+ opacity: 0.5;
521
+ transition: opacity 0.2s ease;
522
+ display: flex !important;
523
+ align-items: center;
524
+ justify-content: center;
525
+ }
526
+
527
+ .ld-file-tree__label:hover .ld-file-tree__icon--chevron {
528
+ opacity: 1;
529
+ }
530
+
531
+ .ld-file-tree__chevron {
532
+ transition: transform 0.2s ease;
533
+ }
534
+
535
+ .ld-file-tree__chevron--open {
536
+ transform: rotate(90deg);
537
+ }
538
+
539
+ .ld-file-tree__icon--spacer {
540
+ width: 14px;
541
+ }
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import type { SVGProps } from "react";
2
2
 
3
3
  const Pnpm = (props: SVGProps<SVGSVGElement>) => (
4
4
  <svg
@@ -54,16 +54,16 @@ const Pnpm = (props: SVGProps<SVGSVGElement>) => (
54
54
  <use xlinkHref="#pnpm_dark__dqv5133G8" fill="#f9ad00" />
55
55
  </g>
56
56
  <g>
57
- <use xlinkHref="#pnpm_dark__b1Lv79ypvm" fill="currentColor" />
57
+ <use xlinkHref="#pnpm_dark__b1Lv79ypvm" fill="#ffffff" />
58
58
  </g>
59
59
  <g>
60
- <use xlinkHref="#pnpm_dark__hy1IZWwLX" fill="currentColor" />
60
+ <use xlinkHref="#pnpm_dark__hy1IZWwLX" fill="#ffffff" />
61
61
  </g>
62
62
  <g>
63
- <use xlinkHref="#pnpm_dark__akQfjxQes" fill="currentColor" />
63
+ <use xlinkHref="#pnpm_dark__akQfjxQes" fill="#ffffff" />
64
64
  </g>
65
65
  <g>
66
- <use xlinkHref="#pnpm_dark__bdSrwE5pk" fill="currentColor" />
66
+ <use xlinkHref="#pnpm_dark__bdSrwE5pk" fill="#ffffff" />
67
67
  </g>
68
68
  </g>
69
69
  </svg>
@@ -1,16 +0,0 @@
1
- import type { SVGProps } from "react";
2
-
3
- const Yarn = (props: SVGProps<SVGSVGElement>) => (
4
- <svg {...props} viewBox="0 0 256 256">
5
- <path
6
- fill="#2C8EBB"
7
- d="M128 0C57.307 0 0 57.307 0 128s57.307 128 128 128 128-57.307 128-128S198.693 0 128 0zm0 234.667C69.195 234.667 21.333 186.805 21.333 128S69.195 21.333 128 21.333 234.667 69.195 234.667 128 186.805 234.667 128 234.667z"
8
- />
9
- <path
10
- fill="#2C8EBB"
11
- d="M173.045 74.053c-4.632-4.632-12.144-4.632-16.776 0L128 102.323l-28.269-28.27c-4.632-4.632-12.144-4.632-16.776 0-4.632 4.632-4.632 12.144 0 16.776L111.224 119.1l-28.269 28.269c-4.632 4.632-4.632 12.144 0 16.776 2.316 2.316 5.352 3.474 8.388 3.474s6.072-1.158 8.388-3.474L128 135.877l28.269 28.268c2.316 2.316 5.352 3.474 8.388 3.474s6.072-1.158 8.388-3.474c4.632-4.632 4.632-12.144 0-16.776L144.776 119.1l28.269-28.271 c4.632-4.632 4.632-12.144 0-16.776z"
12
- />
13
- </svg>
14
- );
15
-
16
- export { Yarn };