mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 20:03:52 +00:00
feat: css editor (#1542)
* feat: allow editing CSS from settings --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me> refactor: style tweaks
This commit is contained in:
committed by
Carlos Valente
parent
ddd629d7db
commit
6817263123
@@ -23,6 +23,7 @@
|
||||
"color": "^4.2.3",
|
||||
"csv-stringify": "^6.4.5",
|
||||
"framer-motion": "^10.10.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.3.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -31,6 +32,7 @@
|
||||
"react-icons": "5.4.0",
|
||||
"react-qr-code": "^2.0.12",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"web-vitals": "^3.1.1",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
@@ -65,6 +67,7 @@
|
||||
"@sentry/vite-plugin": "^2.16.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.8.4",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "catalog:",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
|
||||
const assetsPath = `${apiEntryUrl}/assets`;
|
||||
|
||||
/**
|
||||
* HTTP request to get css contents
|
||||
*/
|
||||
export async function getCSSContents(): Promise<string> {
|
||||
const res = await axios.get(`${assetsPath}/css`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to post css contents
|
||||
*/
|
||||
export async function postCSSContents(css: string): Promise<void> {
|
||||
await axios.post(`${assetsPath}/css`, {
|
||||
css,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to restore default css
|
||||
*/
|
||||
export async function restoreCSSContents(): Promise<string> {
|
||||
const res = await axios.post(`${assetsPath}/css/restore`);
|
||||
return res.data;
|
||||
}
|
||||
@@ -207,6 +207,9 @@ $inner-padding: 1rem;
|
||||
&.end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
&.apart {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animloader {
|
||||
|
||||
@@ -113,8 +113,8 @@ export function BlockQuote({ children }: { children: ReactNode }) {
|
||||
return <blockquote className={style.blockquote}>{children}</blockquote>;
|
||||
}
|
||||
|
||||
export function Error({ children }: { children: ReactNode }) {
|
||||
return <div className={style.fieldError}>{children}</div>;
|
||||
export function Error({ children, className }: { children: ReactNode } & JSX.IntrinsicElements['div']) {
|
||||
return <div className={cx([style.fieldError, className])}>{children}</div>;
|
||||
}
|
||||
|
||||
export function Divider() {
|
||||
@@ -136,7 +136,7 @@ type AllowedInlineTags = 'div' | 'td';
|
||||
type InlineProps<C extends AllowedInlineTags> = {
|
||||
as?: C;
|
||||
relation?: 'inner' | 'component' | 'section';
|
||||
align?: 'start' | 'end';
|
||||
align?: 'start' | 'end' | 'apart';
|
||||
className?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.wrapper {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { forwardRef, memo, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import Editor from 'react-simple-code-editor';
|
||||
import Prism from 'prismjs/components/prism-core';
|
||||
|
||||
import 'prismjs/components/prism-css';
|
||||
import 'prismjs/themes/prism-tomorrow.min.css';
|
||||
import style from './StyleEditor.module.scss';
|
||||
|
||||
interface CodeEditorProps {
|
||||
language: string;
|
||||
initialValue: string;
|
||||
isDirty: boolean;
|
||||
setIsDirty: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const CodeEditor = forwardRef((props: CodeEditorProps, cssRef) => {
|
||||
const { language, initialValue, isDirty, setIsDirty } = props;
|
||||
|
||||
const [code, setCode] = useState(initialValue);
|
||||
|
||||
const highlight = (code: string) => {
|
||||
const grammar = Prism.languages[language];
|
||||
return grammar ? Prism.highlight(code, grammar, language) : code;
|
||||
};
|
||||
|
||||
const handleChange = (newCode: string) => {
|
||||
setCode(newCode);
|
||||
};
|
||||
|
||||
useImperativeHandle(cssRef, () => {
|
||||
return {
|
||||
getCss: () => code,
|
||||
};
|
||||
});
|
||||
|
||||
// add contents to editor on mount and any change in initialValue
|
||||
useEffect(() => {
|
||||
setCode(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
// handle dirty state on change
|
||||
useEffect(() => {
|
||||
if (initialValue.trim() !== code.trim() && !isDirty && code.length !== 0) {
|
||||
setIsDirty(true);
|
||||
}
|
||||
|
||||
if (initialValue.trim() === code.trim() && isDirty) {
|
||||
setIsDirty(false);
|
||||
}
|
||||
}, [initialValue, code, isDirty, setIsDirty]);
|
||||
|
||||
return (
|
||||
<div className={style.wrapper}>
|
||||
<Editor
|
||||
value={code}
|
||||
padding={15}
|
||||
onValueChange={handleChange}
|
||||
highlight={highlight}
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
minHeight: 500,
|
||||
background: '#2d2d2d', // Background of tomorrow theme
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
CodeEditor.displayName = 'StyleEditor';
|
||||
|
||||
export default memo(CodeEditor);
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
.editorActions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.right {
|
||||
align-self: end;
|
||||
text-align: right
|
||||
}
|
||||
|
||||
.column {
|
||||
align-items: start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { lazy, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { getCSSContents, postCSSContents, restoreCSSContents } from '../../../../common/api/assets';
|
||||
import Info from '../../../../common/components/info/Info';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
import style from './StyleEditorModal.module.scss';
|
||||
|
||||
const CodeEditor = lazy(() => import('./StyleEditor'));
|
||||
|
||||
interface CodeEditorModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface CSSRef {
|
||||
getCss: () => string;
|
||||
}
|
||||
|
||||
export default function CodeEditorModal(props: CodeEditorModalProps) {
|
||||
const { isOpen, onClose } = props;
|
||||
|
||||
const [css, setCSS] = useState('');
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const [saveLoading, setSaveLoading] = useState(false);
|
||||
const [resetLoading, setResetLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const cssRef = useRef<CSSRef>(null);
|
||||
|
||||
const handleRestore = async () => {
|
||||
try {
|
||||
setResetLoading(true);
|
||||
const defaultCss = await restoreCSSContents();
|
||||
setCSS(defaultCss);
|
||||
} catch (_error) {
|
||||
/** no error handling for now */
|
||||
} finally {
|
||||
setResetLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setSaveLoading(true);
|
||||
if (cssRef.current) {
|
||||
await postCSSContents(cssRef.current.getCss());
|
||||
setCSS(cssRef.current.getCss());
|
||||
setIsDirty(false);
|
||||
}
|
||||
} catch (_error) {
|
||||
/** no error handling for now */
|
||||
} finally {
|
||||
setSaveLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clear = () => setCSS('');
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchServerCSS() {
|
||||
// check for isOpen to fetch recent css
|
||||
if (isOpen) {
|
||||
try {
|
||||
const css = await getCSSContents();
|
||||
setCSS(css);
|
||||
} catch (_error) {
|
||||
setError('Failed to load CSS from server');
|
||||
/** no error handling for now */
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchServerCSS();
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} variant='ontime' isCentered>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth='max(800px, 40vw)'>
|
||||
<ModalHeader>Edit CSS override</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<CodeEditor ref={cssRef} initialValue={css} language='css' isDirty={isDirty} setIsDirty={setIsDirty} />
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={style.column}>
|
||||
<Info>Invalid CSS will be refused by the browser</Info>
|
||||
{error && <Panel.Error className={style.right}>{`Error: ${error}`}</Panel.Error>}
|
||||
<Panel.InlineElements align='apart' className={style.editorActions}>
|
||||
<Button
|
||||
variant='ontime-ghosted'
|
||||
onClick={handleRestore}
|
||||
isDisabled={saveLoading || resetLoading}
|
||||
isLoading={resetLoading}
|
||||
>
|
||||
Reset to example
|
||||
</Button>
|
||||
<Panel.InlineElements>
|
||||
<Button variant='ontime-ghosted' onClick={clear}>
|
||||
Clear
|
||||
</Button>
|
||||
<Button variant='ontime-subtle' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='ontime-filled'
|
||||
onClick={handleSave}
|
||||
isDisabled={saveLoading || resetLoading || !isDirty}
|
||||
isLoading={saveLoading}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
</Panel.InlineElements>
|
||||
</Panel.InlineElements>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { Button, Input, Switch } from '@chakra-ui/react';
|
||||
import { Button, Input, Switch, useDisclosure } from '@chakra-ui/react';
|
||||
import { ViewSettings } from 'ontime-types';
|
||||
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
@@ -14,11 +14,14 @@ import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import { isOntimeCloud } from '../../../../externals';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
import CodeEditorModal from './StyleEditorModal';
|
||||
|
||||
const cssOverrideDocsUrl = 'https://docs.getontime.no/features/custom-styling/';
|
||||
|
||||
export default function ViewSettingsForm() {
|
||||
const { data, status, refetch } = useViewSettings();
|
||||
const { data: info, status: infoStatus } = useInfo();
|
||||
const { isOpen: isCodeEditorOpen, onOpen: onCodeEditorOpen, onClose: onCodeEditorClose } = useDisclosure();
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -103,6 +106,7 @@ export default function ViewSettingsForm() {
|
||||
<Panel.Section>
|
||||
<Panel.Loader isLoading={isLoading} />
|
||||
<Panel.ListGroup>
|
||||
<CodeEditorModal isOpen={isCodeEditorOpen} onClose={onCodeEditorClose} />
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
title='Override CSS styles'
|
||||
@@ -115,6 +119,15 @@ export default function ViewSettingsForm() {
|
||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
onClick={onCodeEditorOpen}
|
||||
variant='ontime-subtle'
|
||||
size='sm'
|
||||
isDisabled={!data.overrideStyles}
|
||||
width='fit-content'
|
||||
>
|
||||
Edit CSS override
|
||||
</Button>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
<Panel.ListGroup>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
declare module 'prismjs/components/prism-core' {
|
||||
export * from 'prismjs';
|
||||
}
|
||||
|
||||
declare module 'prismjs/components/prism-css';
|
||||
@@ -52,6 +52,7 @@
|
||||
"dev": "cross-env NODE_ENV=development tsx watch ./src/index.ts",
|
||||
"dev:inspect": "cross-env NODE_ENV=development tsx watch --inspect ./src/index.ts",
|
||||
"dev:test": "cross-env IS_TEST=true tsx ./src/index.ts",
|
||||
"prebuild": "tsx ./scripts/bundleCss.ts",
|
||||
"build": "node esbuild.electron.js",
|
||||
"build:electron": "node esbuild.electron.js",
|
||||
"build:local": "node esbuild.dev.js",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { existsSync } from 'fs';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { defaultCss } from '../src/user/styles/bundledCss';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Script to write contents of bundledCss to override.css
|
||||
*/
|
||||
async function bundleCss() {
|
||||
try {
|
||||
const stylesDir = path.resolve(process.cwd(), 'src', 'user', 'styles');
|
||||
const cssFile = path.resolve(stylesDir, 'override.css');
|
||||
|
||||
if (!existsSync(cssFile)) {
|
||||
throw new Error('File does not exist');
|
||||
}
|
||||
|
||||
await writeFile(cssFile, defaultCss, { encoding: 'utf8' });
|
||||
} catch (error) {
|
||||
console.error('Failed writing to CSS file: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
bundleCss();
|
||||
@@ -0,0 +1,41 @@
|
||||
import { defaultCss } from '../../user/styles/bundledCss.js';
|
||||
import type { Request, Response } from 'express';
|
||||
import { readCssFile, writeCssFile } from './assets.service.js';
|
||||
|
||||
/**
|
||||
* Exposes the contents of the cssOverride.css file
|
||||
*/
|
||||
export async function getCssOverride(_req: Request, res: Response) {
|
||||
try {
|
||||
const data = await readCssFile();
|
||||
res.status(200).send(data);
|
||||
} catch (error) {
|
||||
res.status(500).send({ message: error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows modifying the cssOverride.css file
|
||||
*/
|
||||
export async function postCssOverride(req: Request, res: Response) {
|
||||
const { css } = req.body;
|
||||
|
||||
try {
|
||||
await writeCssFile(css);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
res.status(500).send({ message: error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the default cssOverride.css file
|
||||
*/
|
||||
export async function restoreCss(_req: Request, res: Response) {
|
||||
try {
|
||||
await writeCssFile(defaultCss);
|
||||
res.status(200).send(defaultCss);
|
||||
} catch (error) {
|
||||
res.status(500).send({ message: error });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import express from 'express';
|
||||
|
||||
import { getCssOverride, postCssOverride, restoreCss } from './assets.controller.js';
|
||||
import { validatePostCss } from './assets.validation.js';
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
router.get('/css', getCssOverride);
|
||||
router.post('/css', validatePostCss, postCssOverride);
|
||||
router.post('/css/restore', restoreCss);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { publicFiles } from '../../setup/index.js';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { defaultCss } from '../../user/styles/bundledCss.js';
|
||||
|
||||
/**
|
||||
* Reads the user's css file
|
||||
* @returns css contents in the file
|
||||
*/
|
||||
export async function readCssFile(): Promise<string> {
|
||||
const path = publicFiles.cssOverride;
|
||||
if (!existsSync(path)) {
|
||||
await writeFile(path, defaultCss, { encoding: 'utf8' });
|
||||
}
|
||||
|
||||
const css = await readFile(path, { encoding: 'utf8' });
|
||||
|
||||
return css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the user's css file
|
||||
* @param css the updated css to write to file
|
||||
*/
|
||||
export async function writeCssFile(css: string) {
|
||||
const path = publicFiles.cssOverride;
|
||||
if (!existsSync(path)) {
|
||||
await writeFile(path, css, { encoding: 'utf8' });
|
||||
return;
|
||||
}
|
||||
|
||||
await writeFile(path, css, { encoding: 'utf8' });
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { body, validationResult } from 'express-validator';
|
||||
|
||||
export const validatePostCss = [
|
||||
body('css').exists().isString().trim(),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
@@ -12,6 +12,7 @@ import { router as excelRouter } from './excel/excel.router.js';
|
||||
import { router as sessionRouter } from './session/session.router.js';
|
||||
import { router as viewSettingsRouter } from './view-settings/viewSettings.router.js';
|
||||
import { router as reportRouter } from './report/report.router.js';
|
||||
import { router as assetsRouter } from './assets/assets.router.js';
|
||||
|
||||
export const appRouter = express.Router();
|
||||
|
||||
@@ -27,6 +28,7 @@ appRouter.use('/url-presets', urlPresetsRouter);
|
||||
appRouter.use('/session', sessionRouter);
|
||||
appRouter.use('/view-settings', viewSettingsRouter);
|
||||
appRouter.use('/report', reportRouter);
|
||||
appRouter.use('/assets', assetsRouter);
|
||||
|
||||
//we don't want to redirect to react index when using api routes
|
||||
appRouter.all('/*', (_req, res) => {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
export const defaultCss = `
|
||||
/**
|
||||
* This CSS file allows user customisation of the UI
|
||||
* We expose some CSS properties to facilitate this (see below in :root)
|
||||
* In the cases where this is missing, you can add your selectors here
|
||||
*/
|
||||
|
||||
:root {
|
||||
/** Background colour for the views */
|
||||
--background-color-override: #ececec;
|
||||
|
||||
/** Main text colour for the views */
|
||||
--color-override: #101010;
|
||||
|
||||
/** Text colour for the views */
|
||||
--secondary-color-override: #404040;
|
||||
|
||||
/** Accent text colour, used on active elements */
|
||||
--accent-color-override: #fa5656;
|
||||
|
||||
/** Label text colour, used on active elements */
|
||||
--label-color-override: #6c6c6c;
|
||||
|
||||
/** Timer text colour */
|
||||
--timer-color-override: #202020;
|
||||
--timer-warning-color-override: #ffbc56;
|
||||
--timer-danger-color-override: #e69000;
|
||||
--timer-overtime-color-override: #fa5656;
|
||||
--timer-pending-color-override: #578AF4;
|
||||
|
||||
/** Background for card elements on background */
|
||||
--card-background-color-override: #fff;
|
||||
|
||||
/** Font used for all text in views */
|
||||
--font-family-override: 'Open Sans';
|
||||
|
||||
/** Font used for clock in /minimal and /clock views */
|
||||
--font-family-bold-override: 'Arial Black';
|
||||
|
||||
/** Colour used for external message and aux timer in /timer */
|
||||
--external-color-override: #161616;
|
||||
|
||||
/** View specific features: /backstage */
|
||||
/** ---- Background highlight for blink behaviour */
|
||||
--card-background-color-blink-override: #339e4e;
|
||||
/** ---- Colour used for progress bar background */
|
||||
--timer-progress-bg-override: #fff;
|
||||
/** ---- Colour used for progress bar progress */
|
||||
--timer-progress-override: #202020;
|
||||
|
||||
/** View specific features: /op */
|
||||
--operator-customfield-font-size-override: 1.25rem;
|
||||
--operator-running-bg-override: #339e4e;
|
||||
|
||||
/** View specific features: /studio */
|
||||
--studio-active: #101010;
|
||||
--studio-idle: #cfcfcf;
|
||||
--studio-active-label: #101010;
|
||||
--studio-idle-label: #595959;
|
||||
--studio-overtime: #101010;
|
||||
|
||||
/** View specific features: /lower */
|
||||
--lowerThird-font-family-override: 'Courier New';
|
||||
--lowerThird-top-font-weight-override: bold;
|
||||
--lowerThird-bottom-font-weight-override: bold;
|
||||
--lowerThird-top-font-style-override: normal;
|
||||
--lowerThird-bottom-font-style-override: italic;
|
||||
--lowerThird-line-height-override: 1vh;
|
||||
--lowerThird-text-align-override: end;
|
||||
}
|
||||
|
||||
/**
|
||||
* You can inspect the page in your browser and add the selectors here.
|
||||
* In the below example, we change the colour of the overlay message in the stage-timer view.
|
||||
*/
|
||||
.stage-timer > .message-overlay--active > div {
|
||||
color: red;
|
||||
}
|
||||
`;
|
||||
Generated
+31
-3
@@ -6,9 +6,6 @@ settings:
|
||||
|
||||
catalogs:
|
||||
default:
|
||||
'@types/node':
|
||||
specifier: 20.17.16
|
||||
version: 20.17.16
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: 7.16.1
|
||||
version: 7.16.1
|
||||
@@ -143,6 +140,9 @@ importers:
|
||||
framer-motion:
|
||||
specifier: ^10.10.0
|
||||
version: 10.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
prismjs:
|
||||
specifier: ^1.29.0
|
||||
version: 1.29.0
|
||||
react:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1
|
||||
@@ -167,6 +167,9 @@ importers:
|
||||
react-router-dom:
|
||||
specifier: ^6.3.0
|
||||
version: 6.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-simple-code-editor:
|
||||
specifier: ^0.14.1
|
||||
version: 0.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
web-vitals:
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.1
|
||||
@@ -183,6 +186,9 @@ importers:
|
||||
'@types/color':
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
'@types/prismjs':
|
||||
specifier: ^1.26.5
|
||||
version: 1.26.5
|
||||
'@types/react':
|
||||
specifier: ^18.0.26
|
||||
version: 18.0.26
|
||||
@@ -2076,6 +2082,9 @@ packages:
|
||||
'@types/plist@3.0.5':
|
||||
resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==}
|
||||
|
||||
'@types/prismjs@1.26.5':
|
||||
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
|
||||
|
||||
'@types/prop-types@15.7.5':
|
||||
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
||||
|
||||
@@ -4123,6 +4132,10 @@ packages:
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
prismjs@1.29.0:
|
||||
resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
@@ -4278,6 +4291,12 @@ packages:
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
|
||||
react-simple-code-editor@0.14.1:
|
||||
resolution: {integrity: sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
react-style-singleton@2.2.1:
|
||||
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -6850,6 +6869,8 @@ snapshots:
|
||||
xmlbuilder: 15.1.1
|
||||
optional: true
|
||||
|
||||
'@types/prismjs@1.26.5': {}
|
||||
|
||||
'@types/prop-types@15.7.5': {}
|
||||
|
||||
'@types/qs@6.9.7': {}
|
||||
@@ -9287,6 +9308,8 @@ snapshots:
|
||||
|
||||
prettier@3.3.1: {}
|
||||
|
||||
prismjs@1.29.0: {}
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
progress@2.0.3: {}
|
||||
@@ -9431,6 +9454,11 @@ snapshots:
|
||||
'@remix-run/router': 1.2.1
|
||||
react: 18.3.1
|
||||
|
||||
react-simple-code-editor@0.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-style-singleton@2.2.1(@types/react@18.0.26)(react@18.3.1):
|
||||
dependencies:
|
||||
get-nonce: 1.0.1
|
||||
|
||||
Reference in New Issue
Block a user