Update toolbar
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -1,27 +1,20 @@
|
||||
.container {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
width: calc(100% - 64px);
|
||||
margin-inline: 0 16px;
|
||||
margin-left: 32px;
|
||||
margin-right: 32px;
|
||||
margin-top: 112px;
|
||||
margin-bottom: 48px;
|
||||
padding-inline: 40px;
|
||||
padding-block: 48px;
|
||||
border-radius: 0.5rem;
|
||||
padding-inline: 24px;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
width: calc(100% - 332px - 40px);
|
||||
margin-left: 332px;
|
||||
width: calc(100% - 260px);
|
||||
margin-left: 260px;
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Sidebar } from '../components/Sidebar';
|
||||
import { Sidebar } from '@/components/Sidebar/Sidebar';
|
||||
import { Toolbar } from '@/components/Toolbar';
|
||||
import { StickyHeader } from '../components/StickyHeader/StickyHeader';
|
||||
import { Providers } from './providers';
|
||||
@@ -51,10 +51,12 @@ export default async function RootLayout({
|
||||
<body>
|
||||
<Providers>
|
||||
<Sidebar />
|
||||
<Toolbar version={packageVersion} />
|
||||
<StickyHeader />
|
||||
{/* <StickyHeader /> */}
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>{children}</div>
|
||||
<div className={styles.content}>
|
||||
<Toolbar version={packageVersion} />
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<CustomTheme />
|
||||
</Providers>
|
||||
|
||||
@@ -13,7 +13,6 @@ export const PageTitle = ({
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.component}>{type}</div>
|
||||
<MDXRemote
|
||||
components={formattedMDXComponents}
|
||||
source={`# ${title}\n\n${description}`}
|
||||
|
||||
@@ -6,17 +6,24 @@
|
||||
.sidebar {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 112px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 332px;
|
||||
height: calc(100vh - 112px);
|
||||
width: 260px;
|
||||
height: 100vh;
|
||||
color: var(--primary);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.logoContainer {
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
.root {
|
||||
height: 100%;
|
||||
height: calc(100% - 80px);
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
@@ -56,7 +63,7 @@
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 20px 64px;
|
||||
padding: 32px 20px 64px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
RiServiceLine,
|
||||
RiStackLine,
|
||||
} from '@remixicon/react';
|
||||
import { Logo } from './Logo';
|
||||
|
||||
const data = [
|
||||
{
|
||||
@@ -34,6 +35,9 @@ export const Sidebar = () => {
|
||||
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<div className={styles.logoContainer}>
|
||||
<Logo />
|
||||
</div>
|
||||
<ScrollArea.Root className={styles.root}>
|
||||
<ScrollArea.Viewport className={styles.viewport}>
|
||||
<div className={styles.content}>
|
||||
@@ -1,76 +1,59 @@
|
||||
.toolbar {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 32px;
|
||||
right: 32px;
|
||||
height: 112px;
|
||||
z-index: 10;
|
||||
height: 80px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
margin-bottom: 24px;
|
||||
|
||||
.left {
|
||||
/* width: 296px; */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.version {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.versionLinks {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
a {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--secondary);
|
||||
transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out;
|
||||
border-radius: 48px;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
background-color: var(--action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.toolbar {
|
||||
@media (min-width: 768px) {
|
||||
right: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.breadcrumbLink {
|
||||
color: var(--secondary);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
transition: color 0.2s ease-in-out;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbSeparator {
|
||||
color: var(--secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.breadcrumbCurrent {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: none;
|
||||
|
||||
@media (min-width: 320px) {
|
||||
display: flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 820px) {
|
||||
.right {
|
||||
.content {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@@ -79,18 +62,164 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* background-color: var(--action); */
|
||||
border: 1px solid var(--border2);
|
||||
border-radius: 48px;
|
||||
padding-inline: 20px;
|
||||
color: var(--primary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.left {
|
||||
width: 296px;
|
||||
.bubble {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--bg);
|
||||
border: 1px solid var(--border2);
|
||||
border-radius: 32px;
|
||||
padding-inline: 16px;
|
||||
color: var(--primary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
gap: 4px;
|
||||
|
||||
&[data-selected] {
|
||||
background-color: var(--action);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--action);
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&[data-hide-tablet] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
&[data-hide-tablet] {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: 1px solid var(--border2);
|
||||
border-radius: 32px;
|
||||
padding-inline: 4px;
|
||||
height: 32px;
|
||||
display: none;
|
||||
|
||||
@media (min-width: 480px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonGroup button {
|
||||
height: 24px;
|
||||
background-color: var(--bg);
|
||||
border: none;
|
||||
border-radius: 28px;
|
||||
padding-inline: 16px;
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
|
||||
&[data-selected] {
|
||||
background-color: var(--action);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--action);
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.Popup {
|
||||
box-sizing: border-box;
|
||||
padding-block: 0.25rem;
|
||||
border-radius: 0.375rem;
|
||||
background-color: var(--bg);
|
||||
color: var(--color-gray-900);
|
||||
border: 1px solid var(--border);
|
||||
padding-inline: 0.25rem;
|
||||
transform-origin: var(--transform-origin);
|
||||
transition: transform 150ms, opacity 150ms;
|
||||
|
||||
&[data-starting-style],
|
||||
&[data-ending-style] {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
&[data-side='none'] {
|
||||
transition: none;
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
outline: 1px solid var(--color-gray-200);
|
||||
box-shadow: 0px 10px 15px -3px var(--color-gray-200),
|
||||
0px 4px 6px -4px var(--color-gray-200);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
outline: 1px solid var(--color-gray-300);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.Popup[data-trigger='Select'] {
|
||||
min-width: var(--trigger-width);
|
||||
|
||||
.ListBox {
|
||||
display: block;
|
||||
width: unset;
|
||||
max-height: inherit;
|
||||
min-height: unset;
|
||||
border: none;
|
||||
|
||||
.react-aria-Header {
|
||||
padding-left: 1.571rem;
|
||||
}
|
||||
}
|
||||
|
||||
.Item {
|
||||
position: relative;
|
||||
padding: 0 0.571rem 0 1.571rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&[data-focus-visible] {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[data-selected] {
|
||||
font-weight: 600;
|
||||
background: unset;
|
||||
color: var(--text-color);
|
||||
|
||||
&::before {
|
||||
content: '✓';
|
||||
content: '✓' / '';
|
||||
alt: ' ';
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-focused],
|
||||
&[data-pressed] {
|
||||
background: var(--bg);
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,161 @@
|
||||
'use client';
|
||||
|
||||
import { RiGithubLine, RiNpmjsLine } from '@remixicon/react';
|
||||
import { motion, useScroll, useTransform } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
import { Logo } from './Logo';
|
||||
import { ThemeSelector } from './theme';
|
||||
import { ThemeNameSelector } from './theme-name';
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiArrowRightSLine,
|
||||
RiGithubLine,
|
||||
RiMoonLine,
|
||||
RiSunLine,
|
||||
} from '@remixicon/react';
|
||||
import {
|
||||
Button,
|
||||
ListBox,
|
||||
ListBoxItem,
|
||||
Popover,
|
||||
Select,
|
||||
SelectValue,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
} from 'react-aria-components';
|
||||
import styles from './Toolbar.module.css';
|
||||
import { Tabs } from '@base-ui-components/react/tabs';
|
||||
import { usePlayground } from '@/utils/playground-context';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { components, layoutComponents } from '@/utils/data';
|
||||
|
||||
interface ToolbarProps {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export const Toolbar = ({ version }: ToolbarProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { scrollY } = useScroll();
|
||||
const themes = [
|
||||
{ name: 'Backstage', value: 'backstage' },
|
||||
{ name: 'Spotify', value: 'spotify' },
|
||||
{ name: 'Custom theme', value: 'custom' },
|
||||
];
|
||||
|
||||
// Transform scroll velocity to vertical movement
|
||||
const y = useTransform(scrollY, [0, 100], [0, -20], {
|
||||
clamp: false,
|
||||
});
|
||||
export const Toolbar = ({ version }: ToolbarProps) => {
|
||||
const {
|
||||
selectedTheme,
|
||||
setSelectedTheme,
|
||||
selectedThemeName,
|
||||
setSelectedThemeName,
|
||||
} = usePlayground();
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
// Determine breadcrumb content based on current path
|
||||
const getBreadcrumb = () => {
|
||||
const allComponents = [...components, ...layoutComponents];
|
||||
|
||||
// Root page
|
||||
if (pathname === '/') {
|
||||
return { section: null, title: 'Getting Started' };
|
||||
}
|
||||
|
||||
// Components index page
|
||||
if (pathname === '/components') {
|
||||
return { section: null, title: 'Components' };
|
||||
}
|
||||
|
||||
// Component detail pages
|
||||
if (pathname?.startsWith('/components/')) {
|
||||
const slug = pathname.split('/components/')[1];
|
||||
const component = allComponents.find(c => c.slug === slug);
|
||||
return {
|
||||
section: 'Components',
|
||||
sectionLink: '/components',
|
||||
title: component?.title || slug,
|
||||
};
|
||||
}
|
||||
|
||||
// Tokens page
|
||||
if (pathname === '/tokens') {
|
||||
return { section: null, title: 'Tokens' };
|
||||
}
|
||||
|
||||
// Changelog page
|
||||
if (pathname === '/changelog') {
|
||||
return { section: null, title: 'Changelog' };
|
||||
}
|
||||
|
||||
return { section: null, title: '' };
|
||||
};
|
||||
|
||||
const breadcrumb = getBreadcrumb();
|
||||
|
||||
return (
|
||||
<div className={styles.toolbar} ref={containerRef}>
|
||||
<div className={styles.left}>
|
||||
<Logo />
|
||||
<div className={styles.toolbar}>
|
||||
<div className={styles.breadcrumb}>
|
||||
{breadcrumb.section && breadcrumb.sectionLink ? (
|
||||
<>
|
||||
<Link
|
||||
href={breadcrumb.sectionLink}
|
||||
className={styles.breadcrumbLink}
|
||||
>
|
||||
{breadcrumb.section}
|
||||
</Link>
|
||||
<RiArrowRightSLine
|
||||
size={16}
|
||||
className={styles.breadcrumbSeparator}
|
||||
/>
|
||||
<span className={styles.breadcrumbCurrent}>{breadcrumb.title}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className={styles.breadcrumbCurrent}>{breadcrumb.title}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<Select
|
||||
defaultValue="backstage"
|
||||
value={selectedThemeName}
|
||||
onChange={setSelectedThemeName}
|
||||
>
|
||||
<Button className={styles.bubble}>
|
||||
<SelectValue />
|
||||
<RiArrowDownSLine aria-hidden="true" size={16} />
|
||||
</Button>
|
||||
<Popover className={styles.Popup}>
|
||||
<ListBox className={styles.ListBox}>
|
||||
{themes.map(({ name, value }) => (
|
||||
<ListBoxItem key={value} id={value} className={styles.Item}>
|
||||
{name}
|
||||
</ListBoxItem>
|
||||
))}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</Select>
|
||||
<a
|
||||
href="https://www.npmjs.com/package/@backstage/ui"
|
||||
target="_blank"
|
||||
className={styles.bubble}
|
||||
data-hide-tablet
|
||||
>
|
||||
Version {version}
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/backstage/backstage/tree/master/packages/ui"
|
||||
target="_blank"
|
||||
className={styles.bubble}
|
||||
data-hide-tablet
|
||||
>
|
||||
<RiGithubLine size={16} />
|
||||
</a>
|
||||
<ToggleButtonGroup
|
||||
defaultSelectedKeys={['light']}
|
||||
selectedKeys={selectedTheme}
|
||||
onSelectionChange={setSelectedTheme}
|
||||
disallowEmptySelection
|
||||
className={styles.buttonGroup}
|
||||
>
|
||||
<ToggleButton id="light">
|
||||
<RiSunLine aria-hidden="true" size={16} />
|
||||
</ToggleButton>
|
||||
<ToggleButton id="dark">
|
||||
<RiMoonLine aria-hidden="true" size={16} />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<motion.div className={styles.right} style={{ y }}>
|
||||
<div className={styles.version}>Version {version} - Alpha</div>
|
||||
<div className={styles.actions}>
|
||||
<div className={styles.versionLinks}>
|
||||
<a
|
||||
href="https://github.com/backstage/backstage/tree/master/packages/ui"
|
||||
target="_blank"
|
||||
>
|
||||
<RiGithubLine size={16} />
|
||||
</a>
|
||||
<a
|
||||
href="https://www.npmjs.com/package/@backstage/ui"
|
||||
target="_blank"
|
||||
>
|
||||
<RiNpmjsLine size={16} />
|
||||
</a>
|
||||
</div>
|
||||
<ThemeNameSelector />
|
||||
<ThemeSelector />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
.Select {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
height: 3rem;
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1rem;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
border: 1px solid var(--color-gray-200);
|
||||
border-radius: 3rem;
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
color: var(--color-gray-900);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: var(--action);
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-blue);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.Popup {
|
||||
box-sizing: border-box;
|
||||
padding-block: 0.25rem;
|
||||
border-radius: 0.375rem;
|
||||
background-color: var(--bg);
|
||||
color: var(--color-gray-900);
|
||||
border: 1px solid var(--border);
|
||||
padding-inline: 0.25rem;
|
||||
transform-origin: var(--transform-origin);
|
||||
transition: transform 150ms, opacity 150ms;
|
||||
|
||||
&[data-starting-style],
|
||||
&[data-ending-style] {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
&[data-side='none'] {
|
||||
transition: none;
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
outline: 1px solid var(--color-gray-200);
|
||||
box-shadow: 0px 10px 15px -3px var(--color-gray-200),
|
||||
0px 4px 6px -4px var(--color-gray-200);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
outline: 1px solid var(--color-gray-300);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.Popup[data-trigger='Select'] {
|
||||
min-width: var(--trigger-width);
|
||||
|
||||
.ListBox {
|
||||
display: block;
|
||||
width: unset;
|
||||
max-height: inherit;
|
||||
min-height: unset;
|
||||
border: none;
|
||||
|
||||
.react-aria-Header {
|
||||
padding-left: 1.571rem;
|
||||
}
|
||||
}
|
||||
|
||||
.Item {
|
||||
position: relative;
|
||||
padding: 0 0.571rem 0 1.571rem;
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&[data-focus-visible] {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[data-selected] {
|
||||
font-weight: 600;
|
||||
background: unset;
|
||||
color: var(--text-color);
|
||||
|
||||
&::before {
|
||||
content: '✓';
|
||||
content: '✓' / '';
|
||||
alt: ' ';
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-focused],
|
||||
&[data-pressed] {
|
||||
background: var(--bg);
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Button,
|
||||
ListBox,
|
||||
ListBoxItem,
|
||||
Popover,
|
||||
Select,
|
||||
SelectValue,
|
||||
} from 'react-aria-components';
|
||||
import styles from './theme-name.module.css';
|
||||
import { usePlayground } from '@/utils/playground-context';
|
||||
import { RiArrowDownSLine } from '@remixicon/react';
|
||||
|
||||
const themes = [
|
||||
{ name: 'Backstage', value: 'backstage' },
|
||||
{ name: 'Spotify', value: 'spotify' },
|
||||
{ name: 'Custom theme', value: 'custom' },
|
||||
];
|
||||
|
||||
export const ThemeNameSelector = () => {
|
||||
const { selectedThemeName, setSelectedThemeName } = usePlayground();
|
||||
|
||||
return (
|
||||
<Select
|
||||
selectedKey={selectedThemeName || 'backstage'}
|
||||
onSelectionChange={setSelectedThemeName}
|
||||
>
|
||||
<Button className={styles.Select}>
|
||||
<SelectValue />
|
||||
<RiArrowDownSLine aria-hidden="true" />
|
||||
</Button>
|
||||
<Popover className={styles.Popup}>
|
||||
<ListBox className={styles.ListBox}>
|
||||
{themes.map(({ name, value }) => (
|
||||
<ListBoxItem key={value} id={value} className={styles.Item}>
|
||||
{name}
|
||||
</ListBoxItem>
|
||||
))}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
@@ -1,79 +0,0 @@
|
||||
.tabs {
|
||||
border-radius: 0.375rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tabsTheme {
|
||||
width: 100px;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
appearance: none;
|
||||
color: var(--secondary);
|
||||
user-select: none;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
|
||||
&[data-selected] {
|
||||
color: var(--primary);
|
||||
|
||||
& p {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0.25rem 0;
|
||||
border-radius: 0.25rem;
|
||||
outline: 2px solid var(--action);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab p {
|
||||
color: var(--secondary) !important;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
translate: var(--active-tab-left) -50%;
|
||||
width: var(--active-tab-width);
|
||||
height: 3rem;
|
||||
border-radius: 3rem;
|
||||
background-color: var(--action);
|
||||
transition-property: translate, width;
|
||||
transition-duration: 200ms;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Tabs } from '@base-ui-components/react/tabs';
|
||||
import { usePlayground } from '@/utils/playground-context';
|
||||
import styles from './theme.module.css';
|
||||
import { RiMoonLine, RiSunLine } from '@remixicon/react';
|
||||
|
||||
export const ThemeSelector = () => {
|
||||
const { selectedTheme, setSelectedTheme } = usePlayground();
|
||||
|
||||
return (
|
||||
<Tabs.Root
|
||||
className={styles.tabsTheme}
|
||||
onValueChange={setSelectedTheme}
|
||||
value={selectedTheme}
|
||||
>
|
||||
<Tabs.List className={styles.list}>
|
||||
<Tabs.Tab className={styles.tab} value="light">
|
||||
<RiSunLine aria-hidden="true" size={16} />
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab className={styles.tab} value="dark">
|
||||
<RiMoonLine aria-hidden="true" size={16} />
|
||||
</Tabs.Tab>
|
||||
<Tabs.Indicator className={styles.indicator} />
|
||||
</Tabs.List>
|
||||
</Tabs.Root>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ReactNode,
|
||||
useState,
|
||||
useEffect,
|
||||
Key,
|
||||
} from 'react';
|
||||
import { components } from './data';
|
||||
|
||||
@@ -16,8 +17,8 @@ const PlaygroundContext = createContext<{
|
||||
setSelectedScreenSizes: (screenSizes: string[]) => void;
|
||||
selectedComponents: string[];
|
||||
setSelectedComponents: (components: string[]) => void;
|
||||
selectedTheme: Theme;
|
||||
setSelectedTheme: (theme: Theme) => void;
|
||||
selectedTheme: Set<Theme>;
|
||||
setSelectedTheme: (keys: Set<Theme>) => void;
|
||||
selectedThemeName: ThemeName;
|
||||
setSelectedThemeName: (themeName: ThemeName) => void;
|
||||
}>({
|
||||
@@ -25,7 +26,7 @@ const PlaygroundContext = createContext<{
|
||||
setSelectedScreenSizes: () => {},
|
||||
selectedComponents: [],
|
||||
setSelectedComponents: () => {},
|
||||
selectedTheme: 'light',
|
||||
selectedTheme: new Set(['light']),
|
||||
setSelectedTheme: () => {},
|
||||
selectedThemeName: 'backstage',
|
||||
setSelectedThemeName: () => {},
|
||||
@@ -40,16 +41,24 @@ export const PlaygroundProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [selectedComponents, setSelectedComponents] = useState<string[]>(
|
||||
components.map(component => component.slug),
|
||||
);
|
||||
const [selectedTheme, setSelectedTheme] = useState<Theme>('light');
|
||||
const [selectedTheme, setSelectedTheme] = useState<Set<Theme>>(
|
||||
new Set(['light']),
|
||||
);
|
||||
const [selectedThemeName, setSelectedThemeName] =
|
||||
useState<ThemeName>('backstage');
|
||||
|
||||
// Load saved theme from localStorage after hydration
|
||||
useEffect(() => {
|
||||
if (isBrowser) {
|
||||
const savedTheme = localStorage.getItem('theme-mode') as Theme;
|
||||
if (savedTheme) {
|
||||
setSelectedTheme(savedTheme);
|
||||
const savedThemeString = localStorage.getItem('theme-mode');
|
||||
if (savedThemeString) {
|
||||
// Parse the comma-separated string back into a Set
|
||||
const themeArray = savedThemeString
|
||||
.split(',')
|
||||
.filter(Boolean) as Theme[];
|
||||
setSelectedTheme(new Set(themeArray));
|
||||
} else {
|
||||
setSelectedTheme(new Set(['light']));
|
||||
}
|
||||
}
|
||||
}, [isBrowser]);
|
||||
@@ -68,9 +77,9 @@ export const PlaygroundProvider = ({ children }: { children: ReactNode }) => {
|
||||
if (isBrowser) {
|
||||
document.documentElement.setAttribute(
|
||||
'data-theme-mode',
|
||||
selectedTheme || 'light',
|
||||
Array.from(selectedTheme).join(','),
|
||||
);
|
||||
localStorage.setItem('theme-mode', selectedTheme || 'light');
|
||||
localStorage.setItem('theme-mode', Array.from(selectedTheme).join(','));
|
||||
}
|
||||
}, [selectedTheme, isBrowser]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user