Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[experiments] Fully featured Menu + common infra #1330

Merged
merged 33 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
42cce55
[experiments] Fully featured Menu + common infra
michaldudak Jan 13, 2025
912783f
Simplify SettingsPanel
michaldudak Jan 13, 2025
0d10c4f
Merge remote-tracking branch 'upstream/master' into experiments/menu-…
michaldudak Jan 22, 2025
62ffaee
Fix layout issues
michaldudak Jan 22, 2025
5d5b69d
Improved styles
michaldudak Jan 23, 2025
6dd0198
Make the settings panel persistent
michaldudak Jan 23, 2025
9aa1fdb
Merge remote-tracking branch 'upstream/master' into experiments/menu-…
michaldudak Jan 23, 2025
8e1df4f
Don't close settings panel on focusout
michaldudak Jan 23, 2025
ee2621a
Experiment browser and codesandbox editing
michaldudak Jan 30, 2025
a0e546f
Placeholder landing page
michaldudak Jan 30, 2025
b8f25b5
Collapse empty panels
michaldudak Jan 30, 2025
da231c1
Fix issues
michaldudak Jan 30, 2025
89ecbe1
Merge remote-tracking branch 'upstream/master' into experiments/menu-…
michaldudak Jan 30, 2025
72cd8da
Better landing page
michaldudak Jan 30, 2025
f30238d
Support for subdirectories
michaldudak Jan 30, 2025
cebd1b6
Move experiments into subdirectories
michaldudak Jan 30, 2025
d40ffc7
Sort experiments
michaldudak Jan 30, 2025
4cb6f1e
Fix slug generation
michaldudak Jan 31, 2025
3b369ea
lint
michaldudak Jan 31, 2025
09a0a44
Merge branch 'master' into experiments/menu-fully-featured
michaldudak Jan 31, 2025
99d6bcb
Fix page title
michaldudak Jan 31, 2025
734c241
Render controls for text, numbers and lists
michaldudak Feb 3, 2025
aae34c9
Merge remote-tracking branch 'upstream/master' into experiments/menu-…
michaldudak Feb 3, 2025
a01c3a6
Merge remote-tracking branch 'upstream/master' into experiments/menu-…
michaldudak Feb 4, 2025
9d1f4f4
Fixed scrollable sidebar
michaldudak Feb 4, 2025
7c9e858
Make sidebar collapsible
michaldudak Feb 4, 2025
ff5c0fe
Show sidebar button
michaldudak Feb 4, 2025
5223c17
Remove tooltip
michaldudak Feb 4, 2025
0cc8dcf
SSR-friendly settings panel
michaldudak Feb 4, 2025
36b8197
Prettier
michaldudak Feb 5, 2025
1738c1b
Fix netlify build
michaldudak Feb 5, 2025
f70b963
Fix sandboxes
michaldudak Feb 5, 2025
51808a5
Merge remote-tracking branch 'upstream/master' into experiments/menu-…
michaldudak Feb 5, 2025
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
59 changes: 59 additions & 0 deletions docs/src/app/(private)/experiments/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from 'react';
import { type Metadata } from 'next';
import { notFound } from 'next/navigation';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import glob from 'fast-glob';
import { Sidebar } from '../infra/Sidebar';
import classes from '../page.module.css';

const currentDirectory = dirname(fileURLToPath(import.meta.url));
const experimentsRootDirectory = resolve(currentDirectory, '..');

interface Props {
params: Promise<{
slug: string[];
}>;
}

export default async function Page(props: Props) {
const { slug } = await props.params;

const fullPath = resolve(currentDirectory, `../${slug.join('/')}.tsx`);

try {
const Experiment = (await import(`../${slug.join('/')}.tsx`)).default;
return (
<React.Fragment>
<Sidebar experimentPath={fullPath} />
<main className={classes.main}>
<Experiment />
</main>
</React.Fragment>
);
} catch (error) {
notFound();
}
}

export async function generateStaticParams() {
const files = glob.globSync(
['**/*.tsx', '!infra/**/*', '!**/page.tsx', '!**/layout.tsx'],
{ cwd: experimentsRootDirectory },
);

return files.map((file) => {
return {
slug: file.replace(/\.tsx$/, '').split('/'),
};
});
}

export async function generateMetadata(props: Props): Promise<Metadata> {
const params = await props.params;
const { slug } = params;

return {
title: `${slug[slug.length - 1]} - Experiments`,
};
}
44 changes: 0 additions & 44 deletions docs/src/app/(private)/experiments/[slug]/page.tsx

This file was deleted.

30 changes: 0 additions & 30 deletions docs/src/app/(private)/experiments/experiments.module.css

This file was deleted.

15 changes: 15 additions & 0 deletions docs/src/app/(private)/experiments/infra/EditPanel.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.linkButton {
all: unset;
display: block;
width: calc(100% + 8px);
box-sizing: border-box;
color: var(--color-foreground);
padding: 4px 8px;
border-radius: var(--radius-md);
margin-left: -8px;
cursor: pointer;

&:hover {
background-color: var(--color-gray-100);
}
}
23 changes: 23 additions & 0 deletions docs/src/app/(private)/experiments/infra/EditPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as React from 'react';
import { getDependencyFiles } from 'docs/src/components/Demo/DemoLoader';
import { SandboxLink } from './SandboxLink';
import classes from './EditPanel.module.css';

export async function EditPanel(props: EditPanelProps) {
const { experimentPath, ...otherProps } = props;

const dependencies = await getDependencyFiles([experimentPath], true);

return (
<div {...otherProps}>
<h2>Edit</h2>
<SandboxLink files={dependencies} className={classes.linkButton}>
Open in CodeSandbox ({experimentPath})
</SandboxLink>
</div>
);
}

interface EditPanelProps extends React.HTMLAttributes<HTMLDivElement> {
experimentPath: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.list {
display: flex;
flex-direction: column;
gap: 2px;

& a {
display: block;
text-decoration: none;
color: var(--color-foreground);
padding: 4px 8px;
border-radius: var(--radius-md);
margin-left: -8px;

&:hover {
background-color: var(--color-gray-100);
}
}

& h3 {
font-weight: 500;
color: var(--color-gray-500);
margin-top: 2px;
}
}

.groupItems {
margin-left: 8px;
margin-bottom: 0.75em;
}
90 changes: 90 additions & 0 deletions docs/src/app/(private)/experiments/infra/ExperimentsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import * as React from 'react';
import glob from 'fast-glob';
import Link from 'next/link';
import clsx from 'clsx';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { camelToSentenceCase } from 'docs/src/utils/camelToSentenceCase';
import classes from './ExperimentsList.module.css';

const currentDirectory = dirname(fileURLToPath(import.meta.url));
const experimentsRootDirectory = resolve(currentDirectory, '..');

const allExperimentFiles = glob.globSync(
['**/*.tsx', '!infra/**/*', '!**/page.tsx', '!**/layout.tsx'],
{ cwd: experimentsRootDirectory },
);

const groups: Record<string, { name: string; path: string }[]> = {};

for (const key of allExperimentFiles) {
const segments = key.split('/');
let group: string;
let name: string;

if (segments.length === 1) {
group = '*';
name = segments[0];
} else {
group = camelToSentenceCase(segments[0]);
name =
segments[1].toLowerCase().startsWith(`${group.toLowerCase()}-`) &&
segments[1].length > group.length
? segments[1].slice(group.length + 1).trim()
: segments[1].trim();
}

if (!groups[group]) {
groups[group] = [];
}

groups[group].push({
name: camelToSentenceCase(name.replace('.tsx', '').replace(/-/g, ' ')),
path: key.replace('.tsx', ''),
});
}

export function ExperimentsList(props: React.HTMLAttributes<HTMLDivElement>) {
return (
<div {...props} className={clsx(classes.list, props.className)}>
<h2>All experiments</h2>
{Object.keys(groups)
.sort()
.filter((key) => key !== '*')
.map((group: string) => {
return (
<div key={group}>
<h3>{group}</h3>
<ul className={classes.groupItems}>
{groups[group]
.sort((a, b) => a.name.localeCompare(b.name))
.map((experiment) => (
<li key={experiment.path}>
<Link href={`/experiments/${experiment.path}`}>
{experiment.name}
</Link>
</li>
))}
</ul>
</div>
);
})}
{groups['*'] && (
<div>
<h3>Other</h3>
<ul className={classes.groupItems}>
{groups['*']
.sort((a, b) => a.name.localeCompare(b.name))
.map((experiment) => (
<li key={experiment.path}>
<Link href={`/experiments/${experiment.path}`}>
{experiment.name}
</Link>
</li>
))}
</ul>
</div>
)}
</div>
);
}
35 changes: 35 additions & 0 deletions docs/src/app/(private)/experiments/infra/SandboxLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use client';
import * as React from 'react';
import { createCodeSandbox } from 'docs/src/blocks/createCodeSandbox/createCodeSandbox';
import { DemoFile } from 'docs/src/blocks/Demo';

export function SandboxLink(props: SandboxLinkProps) {
const { files, ...otherProps } = props;

const handleClick = React.useCallback(() => {
createCodeSandbox({
demoFiles: files,
demoLanguage: 'ts',
title: 'Base UI experiment',
dependencies: {
react: '^19',
'react-dom': '^19',
},
devDependencies: {
'@types/react': '^19',
'@types/react-dom': '^19',
'react-scripts': 'latest',
},
});
}, [files]);

return (
<button type="button" {...otherProps} onClick={handleClick}>
Open in CodeSandbox
</button>
);
}

interface SandboxLinkProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
files: DemoFile[];
}
Loading