Improve customTheme functionalities

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-01-12 15:45:27 +00:00
parent b81776dfaa
commit 7bc5aaab83
4 changed files with 102 additions and 45 deletions
+1
View File
@@ -49,6 +49,7 @@ iframe {
display: flex;
align-items: center;
justify-content: flex-end;
min-width: 34px;
}
.cm-line {
+1
View File
@@ -13,6 +13,7 @@ import '/public/backstage.css';
export const metadata: Metadata = {
title: 'Canon',
description: 'UI library for Backstage',
metadataBase: new URL('https://canon.backstage.io'),
};
export default function RootLayout({
@@ -6,28 +6,42 @@ import { sass } from '@codemirror/lang-sass';
import styles from './styles.module.css';
import { usePlayground } from '@/utils/playground-context';
import { AnimatePresence, motion } from 'framer-motion';
import { Icon } from '../../../../packages/canon';
const defaultTheme = `:root {
--canon-accent: #000;
}`;
export const CustomTheme = () => {
const [isClient, setIsClient] = useState(false);
const [open, setOpen] = useState(true);
const [customTheme, setCustomTheme] = useState<string | undefined>(undefined);
const { selectedThemeName } = usePlayground();
const [isClient, setIsClient] = useState(false);
const [savedMessage, setSavedMessage] = useState<string>('Save');
const updateStyleElement = (theme: string) => {
let styleElement = document.getElementById(
'custom-theme-style',
) as HTMLStyleElement;
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = 'custom-theme-style';
document.head.appendChild(styleElement);
}
styleElement.textContent = theme;
};
useEffect(() => {
if (selectedThemeName === 'custom') {
const storedTheme = localStorage.getItem('customThemeCss') || '';
setCustomTheme(storedTheme);
let styleElement = document.getElementById(
'custom-theme-style',
) as HTMLStyleElement;
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = 'custom-theme-style';
document.head.appendChild(styleElement);
let storedTheme = localStorage.getItem('customThemeCss');
if (!storedTheme) {
storedTheme = defaultTheme;
localStorage.setItem('customThemeCss', storedTheme);
}
styleElement.textContent = storedTheme;
setCustomTheme(storedTheme);
updateStyleElement(storedTheme);
} else {
const styleElement = document.getElementById(
'custom-theme-style',
@@ -45,48 +59,53 @@ export const CustomTheme = () => {
const handleSave = () => {
if (customTheme) {
localStorage.setItem('customThemeCss', customTheme);
let styleElement = document.getElementById(
'custom-theme-style',
) as HTMLStyleElement;
if (!styleElement) {
styleElement = document.createElement('style');
styleElement.id = 'custom-theme-style';
document.head.appendChild(styleElement);
}
styleElement.textContent = customTheme;
updateStyleElement(customTheme);
setSavedMessage('Saved!');
setTimeout(() => setSavedMessage('Save'), 1000);
}
};
const onChange = useCallback((val: string) => {
const handleChange = useCallback((val: string) => {
setCustomTheme(val);
}, []);
if (isClient === false) return null;
return (
<AnimatePresence>
{isClient && selectedThemeName === 'custom' && (
{selectedThemeName === 'custom' && (
<motion.div
className={styles.container}
className={`${styles.container} ${open ? styles.open : ''}`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
>
<div className={styles.header}>
<div className={styles.headerLeft}>Custom Theme</div>
<button className={styles.button} onClick={handleSave}>
Save
</button>
<div className={styles.headerRight}>
{open && (
<button className={styles.buttonSave} onClick={handleSave}>
{savedMessage}
</button>
)}
<button
className={styles.buttonClose}
onClick={() => setOpen(!open)}
>
<Icon name={open ? 'chevronDown' : 'chevronUp'} />
</button>
</div>
</div>
<div className={styles.editorContainer}>
<CodeMirror
value={customTheme}
height="300px"
extensions={[sass()]}
onChange={handleChange}
className={styles.editor}
basicSetup={{ foldGutter: false }}
/>
</div>
<CodeMirror
value={customTheme}
height="300px"
extensions={[sass()]}
onChange={onChange}
className={styles.editor}
basicSetup={{ foldGutter: false }}
/>
</motion.div>
)}
</AnimatePresence>
@@ -2,21 +2,32 @@
position: fixed;
bottom: 16px;
right: 16px;
width: 36%;
height: 348px;
width: 240px;
height: 47px;
background-color: var(--canon-surface-1);
border-radius: 0.375rem;
border: 1px solid var(--canon-border-base);
display: flex;
flex-direction: column;
overflow: hidden;
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
transition-property: background-color, border-color, height, width;
transition-duration: 0.2s;
transition-timing-function: ease-in-out;
}
.open {
width: 36%;
height: 348px;
}
.editor {
flex: 1;
}
.editorContainer {
overflow: hidden;
}
.header {
height: 46px;
flex-shrink: 0;
@@ -29,7 +40,16 @@
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
}
.button {
.headerLeft {
font-size: 0.875rem;
}
.headerRight {
display: flex;
gap: 8px;
}
.buttonSave {
all: unset;
height: 28px;
padding: 0 8px;
@@ -37,5 +57,21 @@
background-color: #000;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
font-size: 0.75rem;
}
.buttonClose {
all: unset;
height: 28px;
padding: 0 8px;
color: #fff;
background-color: var(--canon-surface-2);
color: var(--canon-text-primary);
transition: background-color 0.2s ease-in-out;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
display: flex;
align-items: center;
justify-content: center;
}