feat(nx-dev): improve tab UX

This commit is contained in:
Juri 2024-07-26 17:50:12 +02:00 committed by Juri Strumpflohner
parent 46dcee6dd2
commit ff51fcd2cd
4 changed files with 80 additions and 42 deletions

View File

@ -4,6 +4,7 @@ import {
ClipboardDocumentIcon,
SparklesIcon,
} from '@heroicons/react/24/outline';
import cx from 'classnames';
import { JSX, ReactNode, useEffect, useState } from 'react';
// @ts-ignore
import { CopyToClipboard } from 'react-copy-to-clipboard';
@ -31,6 +32,7 @@ function CodeWrapper(options: {
title: string;
path: string;
language: string;
isWithinTab?: boolean;
children: string; // intentionally typed as such
}): ({ children }: { children: ReactNode }) => JSX.Element {
return ({ children }: { children: ReactNode }) =>
@ -49,7 +51,11 @@ function CodeWrapper(options: {
title={options.title}
/>
) : (
<CodeOutput content={children} fileName={options.fileName} />
<CodeOutput
content={children}
fileName={options.fileName}
isWithinTab={options.isWithinTab}
/>
);
}
@ -92,6 +98,7 @@ export interface FenceProps {
skipRescope?: boolean;
selectedLineGroup?: string;
onLineGroupSelectionChange?: (selection: string) => void;
isWithinTab?: boolean;
}
export function Fence({
@ -107,6 +114,7 @@ export function Fence({
selectedLineGroup,
skipRescope,
onLineGroupSelectionChange,
isWithinTab,
}: FenceProps) {
if (highlightLines) {
highlightLines = processHighlightLines(highlightLines);
@ -168,7 +176,12 @@ export function Fence({
}
return (
<div className="code-block group relative w-full">
<div
className={cx(
'code-block group relative',
isWithinTab ? '-ml-4 -mr-4 w-[calc(100%+2rem)]' : 'w-auto'
)}
>
<div>
<div className="absolute right-0 top-0 z-10 flex">
{enableCopy && enableCopy === true && (
@ -182,7 +195,7 @@ export function Fence({
type="button"
className={
'not-prose flex border border-slate-200 bg-slate-50/50 p-2 opacity-0 transition-opacity group-hover:opacity-100 dark:border-slate-700 dark:bg-slate-800/60' +
(highlightOptions && highlightOptions[0]
((highlightOptions && highlightOptions[0]) || isWithinTab
? ''
: ' rounded-tr-lg')
}
@ -197,7 +210,7 @@ export function Fence({
)}
{highlightOptions && highlightOptions[0] && (
<Selector
className="rounded-tr-lg"
className={cx(isWithinTab ? '' : 'rounded-tr-lg')}
items={highlightOptions}
selected={selectedOption}
onChange={highlightChange}
@ -219,6 +232,7 @@ export function Fence({
path,
language,
children,
isWithinTab,
})}
/>
</div>

View File

@ -1,14 +1,22 @@
import { JSX, ReactNode } from 'react';
import cx from 'classnames';
export function CodeOutput({
content,
fileName,
isWithinTab,
}: {
content: ReactNode;
fileName: string;
isWithinTab?: boolean;
}): JSX.Element {
return (
<div className="hljs not-prose w-full overflow-x-auto rounded-lg border border-slate-200 bg-slate-50/50 font-mono text-sm dark:border-slate-700 dark:bg-slate-800/60">
<div
className={cx(
'hljs not-prose w-full overflow-x-auto border-slate-200 bg-slate-50/50 font-mono text-sm dark:border-slate-700 dark:bg-slate-800/60',
isWithinTab ? 'border-b border-t' : 'rounded-lg border'
)}
>
{!!fileName && (
<div className="flex border-b border-slate-200 bg-slate-50 px-4 py-2 italic text-slate-400 dark:border-slate-700 dark:bg-slate-800/80 dark:text-slate-500">
{fileName}

View File

@ -34,7 +34,7 @@ export function FenceWrapper(props: FenceProps) {
};
return (
<div className="my-8 w-full">
<div className="w-full">
<Fence {...modifiedProps} />
</div>
);

View File

@ -1,12 +1,12 @@
'use client';
// TODO@ben: refactor to use HeadlessUI tabs
import cx from 'classnames';
import {
import React, {
createContext,
ReactNode,
useContext,
useEffect,
useState,
cloneElement,
} from 'react';
export const TabContext = createContext('');
@ -20,7 +20,8 @@ export function Tabs({
labels: string[];
children: ReactNode;
}) {
const [currentTab, setCurrentTab] = useState(labels[0]);
const [currentTab, setCurrentTab] = useState<string>(labels[0]);
useEffect(() => {
const handleTabSelectedEvent = () => {
const selectedTab = localStorage.getItem(SELECTED_TAB_KEY);
@ -28,43 +29,49 @@ export function Tabs({
setCurrentTab(selectedTab);
}
};
handleTabSelectedEvent();
window.addEventListener(TAB_SELECTED_EVENT, handleTabSelectedEvent);
return () =>
window.removeEventListener(TAB_SELECTED_EVENT, handleTabSelectedEvent);
}, [labels]);
const handleTabClick = (label: string) => {
localStorage.setItem(SELECTED_TAB_KEY, label);
window.dispatchEvent(new Event(TAB_SELECTED_EVENT));
setCurrentTab(label);
};
return (
<TabContext.Provider value={currentTab}>
<section>
<div className="not-prose ">
<div className="border-b border-slate-200 dark:border-slate-800">
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
{labels.map((label: string) => (
<nav className="not-prose -mb-px flex space-x-8" aria-label="Tabs">
{labels.map((label, index) => (
<button
key={label}
role="tab"
aria-selected={label === currentTab}
onClick={() => {
localStorage.setItem(SELECTED_TAB_KEY, label);
window.dispatchEvent(new Event(TAB_SELECTED_EVENT));
setCurrentTab(label);
}}
onClick={() => handleTabClick(label)}
className={cx(
'whitespace-nowrap border-b-2 border-transparent p-2 text-sm font-medium',
'whitespace-nowrap border-b-2 p-2 text-sm font-medium',
label === currentTab
? 'border-blue-500 text-slate-800 dark:border-sky-500 dark:text-slate-300'
: 'text-slate-500 hover:border-blue-500 hover:text-slate-800 dark:text-slate-400 dark:hover:border-sky-500 dark:hover:text-slate-300'
: 'border-transparent text-slate-500 hover:border-blue-500 hover:text-slate-800 dark:text-slate-400 dark:hover:border-sky-500 dark:hover:text-slate-300'
)}
>
{label}
</button>
))}
</nav>
</div>
</div>
<div
className={cx(
'border border-slate-200 pb-2 pl-4 pr-4 pt-2 dark:border-slate-700',
currentTab === labels[0]
? 'rounded-b-md rounded-tr-md'
: 'rounded-b-md rounded-t-md'
)}
>
{children}
</section>
</div>
</TabContext.Provider>
);
}
@ -77,14 +84,23 @@ export function Tab({
children: ReactNode;
}) {
const currentTab = useContext(TabContext);
const isActive = label === currentTab;
if (label !== currentTab) {
return null;
const passPropsToChildren = (children: ReactNode) => {
return React.Children.map(children, (child) => {
if (React.isValidElement(child) && typeof child.type !== 'string') {
return cloneElement(child, { isWithinTab: true });
}
return child;
});
};
return (
<div className="prose prose-slate dark:prose-invert mt-4 max-w-none">
{children}
<div
className="prose prose-slate dark:prose-invert mt-2 max-w-none"
hidden={!isActive}
>
{isActive && passPropsToChildren(children)}
</div>
);
}