Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions fern/assets/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1164,3 +1164,26 @@ h1, h2, h3 {
}
}
/*** END -- HIGHLIGHTED FRAME STYLING ***/

/*** FILES COMPONENT STYLING ***/
.files-row-highlighted {
background-color: rgba(255, 235, 59, 0.15);
}

@media (prefers-color-scheme: dark) {
.files-row-highlighted {
background-color: rgba(255, 235, 59, 0.1);
}
}

.files-row-comment {
color: #6b7280;
opacity: 0.8;
}

@media (prefers-color-scheme: dark) {
.files-row-comment {
color: #9ca3af;
}
}
/*** END -- FILES COMPONENT STYLING ***/
69 changes: 69 additions & 0 deletions fern/components/File.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react';

export interface FileProps {
name: string;
href?: string;
highlighted?: boolean;
comment?: string;
className?: string;
}

export const File: React.FC<FileProps> = ({
name,
href,
highlighted = false,
comment,
className = ''
}) => {
const baseClasses = "grid items-center gap-2 py-1 px-2 rounded transition-colors";
const gridClasses = "grid-cols-[24px_24px_1fr_auto]";
const combinedClasses = `${baseClasses} ${gridClasses} ${className}`.trim();

const highlightStyle = highlighted ? { backgroundColor: 'rgba(255, 235, 59, 0.15)' } : {};
const formattedComment = comment ? (comment.startsWith('#') ? comment : `# ${comment}`) : null;

const content = (
<>
{/* Empty spacer for chevron column */}
<div className="w-6" />

{/* File icon */}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-file flex-shrink-0 text-(color::--grayscale-a11)">
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/>
<path d="M14 2v4a2 2 0 0 0 2 2h4"/>
</svg>

{/* File name */}
<span className="text-default text-sm font-mono">{name}</span>

{/* Comment */}
{formattedComment && (
<span
className="text-xs font-mono whitespace-nowrap overflow-hidden text-ellipsis"
style={{ color: '#6b7280', opacity: 0.8 }}
title={formattedComment}
>
{formattedComment}
</span>
)}
</>
);

if (href) {
return (
<a
href={href}
className={`${combinedClasses} hover:underline cursor-pointer no-underline`}
style={highlightStyle}
>
{content}
</a>
);
}

return (
<div className={combinedClasses} style={highlightStyle}>
{content}
</div>
);
};
14 changes: 14 additions & 0 deletions fern/components/Files.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';

export interface FilesProps {
children: React.ReactNode;
className?: string;
}

export const Files: React.FC<FilesProps> = ({ children, className = '' }) => {
return (
<div className={`space-y-1 ${className}`.trim()}>
{children}
</div>
);
};
111 changes: 111 additions & 0 deletions fern/components/Folder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { useState } from 'react';

export interface FolderProps {
name: string;
defaultOpen?: boolean;
href?: string;
highlighted?: boolean;
comment?: string;
children?: React.ReactNode;
className?: string;
}

export const Folder: React.FC<FolderProps> = ({
name,
defaultOpen = false,
href,
highlighted = false,
comment,
children,
className = ''
}) => {
const [isOpen, setIsOpen] = useState(defaultOpen);

const baseClasses = "grid items-center gap-2 py-1 px-2 rounded transition-colors";
const gridClasses = "grid-cols-[24px_24px_1fr_auto]";
const combinedClasses = `${baseClasses} ${gridClasses}`.trim();

const highlightStyle = highlighted ? { backgroundColor: 'rgba(255, 235, 59, 0.15)' } : {};

const handleToggle = () => {
setIsOpen(!isOpen);
};

const formattedComment = comment ? (comment.startsWith('#') ? comment : `# ${comment}`) : null;

const folderIcon = isOpen ? (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-folder-open flex-shrink-0 text-(color::--grayscale-a11)">
<path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-folder flex-shrink-0 text-(color::--grayscale-a11)">
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>
</svg>
);

const caretIcon = (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={`lucide lucide-chevron-right flex-shrink-0 text-(color::--grayscale-a11) transition-transform ${isOpen ? 'rotate-90' : ''}`}
style={{ transition: 'transform 0.2s' }}
>
<path d="m9 18 6-6-6-6"/>
</svg>
);

const content = (
<>
{caretIcon}
{folderIcon}
<span className="text-default text-sm font-mono">{name}</span>
{formattedComment && (
<span
className="text-xs font-mono whitespace-nowrap overflow-hidden text-ellipsis"
style={{ color: '#6b7280', opacity: 0.8 }}
title={formattedComment}
>
{formattedComment}
</span>
)}
</>
);

const header = href ? (
<a
href={href}
className={`${combinedClasses} hover:underline cursor-pointer no-underline`}
style={highlightStyle}
>
{content}
</a>
) : (
<button
onClick={handleToggle}
className={`${combinedClasses} w-full text-left cursor-pointer border-0 bg-transparent`}
style={highlightStyle}
aria-expanded={isOpen}
type="button"
>
{content}
</button>
);

return (
<div className={className}>
{header}
{isOpen && children && (
<div className="pl-6 space-y-0.5 mt-0.5">
{children}
</div>
)}
</div>
);
};
3 changes: 3 additions & 0 deletions fern/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { File } from './File';
export { Folder } from './Folder';
export { Files } from './Files';
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,42 @@ Use the `defaultOpen` property to have specific folders expanded when the page l
</Files>
```

### Highlighted files and folders with comments

Use the `highlighted` property to visually emphasize specific files or folders, and the `comment` property to add explanatory text that appears to the right of the item. This is useful for drawing attention to important files or providing additional context.

<div className="highlight-frame">
<Files>
<Folder name="components" defaultOpen highlighted comment="Main component directory">
<File name="accordion.mdx" />
<File name="button.mdx" highlighted comment="Primary button component" />
<File name="card.mdx" />
<File name="tabs.mdx" />
</Folder>
<Folder name="assets">
<File name="styles.css" highlighted comment="Global styles" />
</Folder>
<File name="markdown.mdx" href="/learn/docs/writing-content/markdown" />
<File name="README.md" highlighted comment="Start here" />
</Files>
</div>

```jsx Markdown maxLines=10
<Files>
<Folder name="components" defaultOpen highlighted comment="Main component directory">
<File name="accordion.mdx" />
<File name="button.mdx" highlighted comment="Primary button component" />
<File name="card.mdx" />
<File name="tabs.mdx" />
</Folder>
<Folder name="assets">
<File name="styles.css" highlighted comment="Global styles" />
</Folder>
<File name="markdown.mdx" href="/learn/docs/writing-content/markdown" />
<File name="README.md" highlighted comment="Start here" />
</Files>
```

## Properties

### `<Files>` properties
Expand Down Expand Up @@ -195,6 +231,14 @@ Use the `defaultOpen` property to have specific folders expanded when the page l
Optional URL to make the folder name clickable. The name will show an underline on hover when a link is provided.
</ParamField>

<ParamField path="highlighted" type="boolean" default={false}>
Whether the folder should be visually highlighted. Use this to draw attention to important directories.
</ParamField>

<ParamField path="comment" type="string" required={false}>
Optional comment text that appears to the right of the folder name. Use this to provide additional context or explanations.
</ParamField>

<ParamField path="className" type="string" required={false}>
Optional CSS class name for custom styling.
</ParamField>
Expand All @@ -209,6 +253,14 @@ Use the `defaultOpen` property to have specific folders expanded when the page l
Optional URL to make the file name clickable. The name will show an underline on hover when a link is provided.
</ParamField>

<ParamField path="highlighted" type="boolean" default={false}>
Whether the file should be visually highlighted. Use this to draw attention to important files.
</ParamField>

<ParamField path="comment" type="string" required={false}>
Optional comment text that appears to the right of the file name. Use this to provide additional context or explanations.
</ParamField>

<ParamField path="className" type="string" required={false}>
Optional CSS class name for custom styling.
</ParamField>