feat(nx-dev): improve tab UX
This commit is contained in:
parent
46dcee6dd2
commit
ff51fcd2cd
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -34,7 +34,7 @@ export function FenceWrapper(props: FenceProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-8 w-full">
|
||||
<div className="w-full">
|
||||
<Fence {...modifiedProps} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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) => (
|
||||
<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);
|
||||
}}
|
||||
className={cx(
|
||||
'whitespace-nowrap border-b-2 border-transparent 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'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<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={() => handleTabClick(label)}
|
||||
className={cx(
|
||||
'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'
|
||||
: '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
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user