use-theme-editor
Version:
Zero configuration CSS variables based theme editor
281 lines (258 loc) • 10 kB
JavaScript
import React, {createContext, Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
import {ROOT_SCOPE, useThemeEditor} from '../hooks/useThemeEditor';
import {useLocalStorage} from '../hooks/useLocalStorage';
import {useServerThemes} from '../hooks/useServerThemes';
import {ResizableFrame} from './ResizableFrame';
import {ServerThemesList} from './ui/ServerThemesList';
import {CustomVariableInput} from './ui/CustomVariableInput';
import {StylesheetDisabler} from './ui/StylesheetDisabler';
import {PropertyCategoryFilter} from './ui/PropertyCategoryFilter';
import {isColorProperty} from './inspector/TypedControl';
import {PropertySearch} from './ui/PropertySearch';
import {Checkbox} from './controls/Checkbox';
import {ToggleButton} from './controls/ToggleButton';
import {ImportExportTools} from './ui/ImportExportTools';
import {ThemeUploadPanel} from './ui/ThemeUploadPanel';
import {MovablePanels} from './movable/MovablePanels';
import {FrameSizeSettings} from './ui/FrameSizeSettings';
import {ScreenSwitcher} from './ui/ScreenSwitcher';
import {ThemeEditorExtraOptions} from './ui/ThemeEditorExtraOptions';
import {MoveControls} from './movable/MoveControls';
import {Area} from './movable/Area';
import {FrameScaleSlider} from './ui/FrameScaleSlider';
import {Drawer} from './movable/Drawer';
import {CurrentTheme} from './ui/CurrentTheme';
import { RemoveAnnoyingPrefix } from './inspector/RemoveAnnoyingPrefix';
import { NameReplacements } from './inspector/NameReplacements';
import { updateScopedVars } from '../initializeThemeEditor';
import { HistoryControls } from './ui/HistoryControls';
import { useResumableState } from '../hooks/useResumableReducer';
import { useInsertionEffect } from 'react';
import { SmallFullHeightFrame } from './SmallFullHeightFrame';
import { Inspector } from './ui/Inspector';
import { get, use } from '../state';
import { Hotkeys } from './Hotkeys';
import { ColorSettings } from './ui/ColorSettings';
import { InformationVisibilitySettings } from './ui/InformationVisibilitySettings';
import { WebpackHomeInput } from './ui/WebpackHomeInput';
export const ThemeEditorContext = createContext({});
export const prevGroups = [];
let prevOpengroups = null;
export const ThemeEditor = (props) => {
const {
config,
groups: _unfilteredGroups,
allVars,
defaultValues,
lastInspectTime,
inspectedIndex,
isNewInspection,
} = props;
const { fileName } = get;
const unfilteredGroups = prevGroups[currentInspected] || _unfilteredGroups;
const [currentInspected, setCurrentInspected] = useResumableState(-1, 'inspected-index');
const [_openGroups, setOpenGroups] = useResumableState({}, 'OPEN_GROUPS');
const openGroups = prevOpengroups !== null ? prevOpengroups : _openGroups;
prevOpengroups = null;
useLayoutEffect(() => {
if (currentInspected !== -1 && currentInspected !== inspectedIndex) {
// window.requestAnimationFrame(() => {
frameRef.current?.contentWindow.postMessage(
{
type: 'inspect-previous',
payload: { index: currentInspected },
},
window.location.origin
);
// });
prevOpengroups = openGroups;
}
}, [currentInspected]);
useLayoutEffect(() => {
if ( isNewInspection ) {
prevGroups[inspectedIndex] = unfilteredGroups;
setCurrentInspected(inspectedIndex);
}
}, [inspectedIndex]);
// Open first group.
// I don't like how this is done but it's tricky to replace at the moment.
useLayoutEffect(() => {
if (isNewInspection && openFirstOnInspect && unfilteredGroups.length > 0) {
setOpenGroups(
{
[unfilteredGroups[0].label]: true,
},
{ skipHistory: true }
);
}
}, [unfilteredGroups, openFirstOnInspect]);
const [
{
scopes,
// changeRequiresReset,
},
dispatch,
] = useThemeEditor({allVars, defaultValues});
const frameRef = useRef(null);
const scrollFrameRef = useRef(null);
useInsertionEffect(() => {
updateScopedVars(scopes, true);
}, [scopes]);
useEffect(() => {
frameRef.current.contentWindow.postMessage(
{
type: 'set-scopes-styles',
payload: { scopes, resetAll: true },
},
window.location.origin,
);
scrollFrameRef.current?.contentWindow.postMessage(
{
type: 'set-scopes-styles',
payload: { scopes, resetAll: true },
},
window.location.origin,
);
}, [scopes]);
// Don't move to settings yet, hiding and showing of panels probably needs a different solution.
const [importDisplayed, setImportDisplayed] = useState(false);
const [serverThemesDisplayed, setServerThemesDisplayed] = useLocalStorage('server-themes-displayed', true);
const [sheetsDisablerDisplayed, setSheetDisablerDisplayed] = useState(false);
const [openFirstOnInspect, setOpenFirstOnInspect] = useLocalStorage('open-first-inspect', true);
const {
serverThemes,
serverThemesLoading,
uploadTheme,
deleteTheme,
} = useServerThemes(config.serverThemes);
const existsOnServer = serverThemes && fileName in serverThemes;
const modifiedServerVersion = useMemo(() => {
return existsOnServer && JSON.stringify(scopes) !== JSON.stringify(serverThemes[fileName].scopes);
}, [serverThemes[fileName]?.scopes, scopes]);
const [fullPagePreview, setFullPagePreview] = useLocalStorage('full-page-preview', false)
return (
<ThemeEditorContext.Provider
value={{
allVars,
dispatch,
defaultValues,
frameRef,
scrollFrameRef,
serverThemes,
serverThemesLoading,
uploadTheme,
deleteTheme,
existsOnServer,
modifiedServerVersion,
setSheetDisablerDisplayed,
scopes,
lastInspectTime,
openGroups, setOpenGroups,
}}
>
<Hotkeys {...{modifiedServerVersion, scopes, uploadTheme, frameRef}}/>
<div className="theme-editor">
<MovablePanels stateHook={use.uiArrangement}>
<div
style={{
display: 'flex',
columns: 2,
justifyContent: 'space-between',
}}
>
<Area
id="area-top"
style={{ justifyContent: 'flex-start', flexGrow: 1 }}
>
<div style={{
display: 'flex',
alignItems: 'flex-start',
}}>
<PropertyCategoryFilter/>
<PropertySearch/>
</div>
<ScreenSwitcher />
</Area>
<Area
id="area-top-reverse"
style={{
flexDirection: 'row-reverse',
justifyContent: 'flex-start',
flexGrow: 1,
}}
>
<FrameScaleSlider/>
<div className={'theme-editor-menu'}>
<ToggleButton controls={[importDisplayed, setImportDisplayed]}>
Import/export
</ToggleButton>
<ToggleButton controls={[sheetsDisablerDisplayed, setSheetDisablerDisplayed]}>
Stylesheets
</ToggleButton>
<ToggleButton controls={[serverThemesDisplayed, setServerThemesDisplayed]}>
Server
</ToggleButton>
</div>
</Area>
</div>
<div style={{display: 'flex', justifyContent: 'space-between', flexGrow: '1', gap: '16px'}}>
<Area id="area-left">
<MoveControls />
<Inspector {...{unfilteredGroups}}/>
</Area>
<ResizableFrame src={window.location.href} />
{!!fullPagePreview && <SmallFullHeightFrame src={window.location.href} />}
<Area id="area-right">
<Fragment>
{serverThemesDisplayed && <ServerThemesList/>}
</Fragment>
<Fragment>{sheetsDisablerDisplayed && <StylesheetDisabler />}</Fragment>
<Fragment>{importDisplayed && <ImportExportTools />}</Fragment>
<ThemeUploadPanel/>
<InformationVisibilitySettings />
<ColorSettings />
<div>
<Checkbox
// id={'full-page-preview'}
controls={[fullPagePreview, setFullPagePreview]}
title='This does not work properly for pages that have different styles based on screen height.'
>Scroll preview</Checkbox>
<Checkbox
// id={'open-first-on-inspect'}
controls={[openFirstOnInspect, setOpenFirstOnInspect]}
>Auto open first group on inspect</Checkbox>
<WebpackHomeInput />
</div>
<HistoryControls />
</Area>
</div>
<div
style={{
display: 'flex',
columns: 2,
justifyContent: 'space-between',
flexGrow: 0,
alignItems: 'flex-end',
}}
>
<Area id="area-bottom"></Area>
<Area
id="area-bottom-reverse"
style={{
flexDirection: 'row-reverse',
}}
></Area>
<Drawer>
<CustomVariableInput/>
<FrameSizeSettings />
<ThemeEditorExtraOptions />
<RemoveAnnoyingPrefix />
{/* <ExampleTabs/> */}
<NameReplacements/>
</Drawer>
</div>
</MovablePanels>
</div>
</ThemeEditorContext.Provider>
);
};