ui: better config page layout

This commit is contained in:
undefined committed 2025-03-25 18:27:43 +08:00
1 parent 4421f98620
commit f305110381
27 files changed
+748 -377

No files matched your search

@@ -0,0 +1,162 @@
import 'schemastery-react/lib/schemastery-react.css';
import { Allotment } from 'allotment';
import { diffLines } from 'diff';
import yaml from 'js-yaml';
import React from 'react';
import { createSchemasteryReact } from 'schemastery-react';
import { ComponentsContext } from './provider';
const locales = {
zh: 'zh-CN',
en: 'en-US',
};
function MonacoContainer({
config, setValue, setError, monaco, schema, editorCallback, size,
}) {
const editorRef = React.useRef<HTMLDivElement>(null);
const [loading, setLoading] = React.useState(false);
const [editor, setEditor] = React.useState<any>(null);
const [model, setModel] = React.useState<any>(null);
const { codeFontFamily } = React.useContext(ComponentsContext);
React.useEffect(() => {
if (!monaco) return;
const interval = setInterval(() => {
monaco.editor.remeasureFonts();
}, 1000);
return () => clearInterval(interval); // eslint-disable-line consistent-return
}, [monaco]);
React.useEffect(() => {
const listener = () => {
if (!editor) return;
editor.layout();
};
window.addEventListener('resize', listener);
return () => window.removeEventListener('resize', listener);
}, [editor]);
React.useEffect(() => {
if (!editor) return;
editor.layout();
}, [size]);
React.useEffect(() => {
if (!editor) return;
const disposable = editor.onDidChangeModelContent(() => {
const val = editor.getValue({ lineEnding: '\n', preserveBOM: false });
try {
const loaded = yaml.load(val);
schema(loaded);
setValue(val);
setError('');
} catch (err) {
setError(err.message);
}
});
return () => disposable.dispose(); // eslint-disable-line
}, [editor, setValue, setError]);
React.useEffect(() => {
if (!editorRef.current || loading) return;
setLoading(true);
// eslint-disable-next-line
const model = monaco.editor.createModel(config, 'yaml', monaco.Uri.parse('hydro://system/setting.yaml'));
setModel(model);
const e = monaco.editor.create(editorRef.current, {
theme: 'vs-light',
lineNumbers: 'off',
glyphMargin: true,
lightbulb: { enabled: monaco.editor.ShowLightbulbIconMode.On },
model,
minimap: { enabled: false },
hideCursorInOverviewRuler: true,
overviewRulerLanes: 0,
overviewRulerBorder: false,
fontFamily: codeFontFamily,
fontLigatures: '',
unicodeHighlight: {
ambiguousCharacters: true,
},
});
editorCallback(e, model, editorRef.current);
setEditor(e);
}, [editorRef.current]);
React.useEffect(() => {
if (!editor) return;
const current = editor.getValue({ lineEnding: '\n', preserveBOM: false });
const diff = diffLines(current, config);
const ops: { range: any; text: string }[] = [];
let cursor = 1;
for (const line of diff) {
if (line.added) {
let range = model.getFullModelRange();
range = range.setStartPosition(cursor, 0);
range = range.setEndPosition(cursor, 0);
ops.push({ range, text: line.value });
} else if (line.removed) {
let range = model.getFullModelRange();
range = range.setStartPosition(cursor, 0);
cursor += line.count;
range = range.setEndPosition(cursor, 0);
ops.push({ range, text: '' });
} else cursor += line.count;
}
model.pushEditOperations([], ops, undefined);
}, [editor, config]);
return (
<div ref={editorRef} style={{
width: '100%', height: '500px',
}} />
);
}
export default function ConfigEditor({
schema, config, monaco, Markdown, onSave, registerAction, sidebar,
}) {
const [value, setValue] = React.useState(yaml.load(config));
const [info, setInfo] = React.useState('');
const [stringConfig, setStringConfig] = React.useState(config);
const initial = React.useMemo(() => yaml.load(config), []);
const { i18n } = React.useContext(ComponentsContext);
const Form = React.useMemo(() => createSchemasteryReact({
locale: locales[i18n('__id')] || 'en-US',
Markdown,
}), []);
const updateFromForm = React.useCallback((v) => {
const newDump = yaml.dump(v);
if (newDump === stringConfig) return;
setStringConfig(newDump);
setValue(v);
}, [stringConfig]);
const updateFromMonaco = React.useCallback((v) => {
if (v === stringConfig) return;
setStringConfig(v);
setValue(yaml.load(v));
}, [stringConfig]);
const [size, setSize] = React.useState([50, 50]);
return (<div className="fullscreen-content" style={{ backgroundColor: 'white', zIndex: 10 }}>
<Allotment onChange={setSize}>
<Allotment.Pane>
<MonacoContainer
editorCallback={registerAction} schema={schema} monaco={monaco}
config={stringConfig} setValue={updateFromMonaco} setError={setInfo}
size={size[0]}
/>
<pre className="help-text">{info}</pre>
<button onClick={() => onSave(stringConfig)} className="rounded primary button">{i18n('Save All Changes')}</button>
</Allotment.Pane>
<Allotment.Pane>
<div style={{
overflowY: 'scroll', top: 0, bottom: 0, left: 0, right: 0, position: 'absolute', marginLeft: 10,
}}>
<Form schema={schema} initial={initial} value={value} onChange={updateFromForm} />
</div>
</Allotment.Pane>
{sidebar && <Allotment.Pane maxSize={220}>{sidebar}</Allotment.Pane>}
</Allotment>
</div>);
}
+20
View File
@@ -0,0 +1,20 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
export default function IconComponent(props: { name: string, className?: string } & React.HTMLAttributes<HTMLSpanElement>) {
const {
name,
className,
...rest
} = props;
const cn = classNames(className, `icon icon-${name}`);
return (
<span {...rest} className={cn} />
);
}
IconComponent.propTypes = {
name: PropTypes.string.isRequired,
className: PropTypes.string,
};
+13
View File
@@ -0,0 +1,13 @@
import React from 'react';
export function Row({ children }: { children: React.ReactNode }) {
return <div className="row">
{children}
</div>;
}
export function Column({ children, md }: { children: React.ReactNode, md?: number }) {
return <div className={`columns medium-${md || 12}`}>
{children}
</div>;
}
@@ -0,0 +1,26 @@
import { toast } from 'react-toastify';
export default class Notification {
type: string;
action: any;
$dom: JQuery<HTMLElement>;
$n: JQuery<HTMLElement>;
duration: number;
autoHideTimer?: NodeJS.Timeout;
static async success(message: string, duration?: number) {
return toast.success(message, { autoClose: duration });
}
static async info(message: string, duration?: number) {
return toast.info(message, { autoClose: duration });
}
static async warn(message: string, duration?: number) {
return toast.warning(message, { autoClose: duration });
}
static async error(message: string, duration?: number) {
return toast.error(message, { autoClose: duration });
}
}
@@ -0,0 +1,351 @@
import './autocomplete.scss';
import { debounce, uniqueId } from 'lodash';
import React, {
forwardRef, useEffect,
useImperativeHandle, useRef, useState,
} from 'react';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import Icon from '../Icon';
export interface AutoCompleteProps<Item> {
width?: string;
/**
* if you need fix height, set to at least "30px"
* for Hydro, no less then "34px" can be better
*/
height?: string;
disabled?: boolean;
placeholder?: string;
disabledHint?: string;
listStyle?: React.CSSProperties;
cacheKey?: string;
renderItem?: (item: Item) => any;
queryItems?: (query: string) => Promise<Item[]> | Item[];
fetchItems?: (ids: string[]) => Promise<Item[]> | Item[];
itemText?: (item: Item) => string;
itemKey?: (item: Item) => string;
onChange?: (value: string) => any;
multi?: boolean;
draggable?: boolean;
selectedKeys?: string[];
allowEmptyQuery?: boolean;
freeSolo?: boolean;
freeSoloConverter?: (value: string) => string;
}
export interface AutoCompleteHandle<Item> {
getSelectedItems: () => Item[];
getSelectedItemKeys: () => string[];
getSelectedItemsAsString: () => string;
setSelectedItems: (items: Item[]) => void;
getQuery: () => string;
setQuery: (query: string) => void;
setSelectedKeys: (keys: string[]) => void;
triggerQuery: () => any;
closeList: () => void;
/** returns comma seperated value text */
getValue: () => string;
/** return value detail */
getValueArray: () => Item[];
clear: () => void;
focus: () => void;
}
const superCache = {};
function DraggableSelection({
type, id, move, children, ...props
}) {
const ref = useRef<HTMLDivElement>(null);
const [isDragging, drag] = useDrag<{ id: string }, any, boolean>(() => ({
type,
item: { id },
collect: (m) => m.isDragging(),
}));
const [, drop] = useDrop({
accept: type,
hover: (item: { id: string }) => {
if (!ref.current) return;
move(item.id, id);
},
});
drag(drop(ref));
return (
<div ref={ref} {...props} style={{ opacity: isDragging ? 0.2 : 1 }}>
{children}
</div>
);
}
// eslint-disable-next-line prefer-arrow-callback
const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, ref: React.Ref<AutoCompleteHandle<T>>) {
const {
multi = false, width = '100%', height = 'auto',
freeSolo = false, allowEmptyQuery = false, listStyle = {}, // eslint-disable-line @eslint-react/no-unstable-default-props
disabled = false, disabledHint = '', draggable = multi,
} = props;
const queryItems = props.queryItems ?? (() => []);
const renderItem = props.renderItem ?? ((item) => item);
const itemText = props.itemText ?? ((item) => item.toString());
const itemKey = props.itemKey ?? itemText;
const onChange = props.onChange ?? (() => { });
const freeSoloConverter = freeSolo ? props.freeSoloConverter ?? ((i) => i) : ((i) => i);
const [focused, setFocused] = useState(false); // is focused
const [selected, setSelected] = useState([]); // selected items
const [selectedKeys, setSelectedKeys] = useState(props.selectedKeys || []); // keys of selected items
const [itemList, setItemList] = useState([]); // items list
const [currentItem, setCurrentItem] = useState(null); // index of current item (in item list)
const [rerender, setRerender] = useState(false);
const [draggableId] = useState(uniqueId());
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLUListElement>(null);
let [queryCache, valueCache] = [useRef({}).current, useRef({}).current];
if (props.cacheKey) {
superCache[props.cacheKey] ||= { query: {}, value: {} };
queryCache = superCache[props.cacheKey].query;
valueCache = superCache[props.cacheKey].value;
}
const queryList = async (query) => {
if (!query && !allowEmptyQuery) {
setItemList([]);
setCurrentItem(null);
return;
}
queryCache[query] ||= await queryItems(query);
for (const item of queryCache[query]) valueCache[itemKey(item)] = item;
setItemList(queryCache[query]);
setCurrentItem((!freeSolo && queryCache[query].length) ? 0 : null);
};
useEffect(() => {
setSelectedKeys(props.selectedKeys || []);
}, [JSON.stringify(props.selectedKeys)]);
const dispatchChange = () => {
if (!multi) onChange(inputRef.current?.value);
else onChange(selectedKeys.filter((v) => v?.trim().length).join(','));
};
let first = !multi;
useEffect(() => {
if (first) first = false;
else dispatchChange();
if (!multi) return; // Load pre-selected items only in multi mode
const ids = [];
for (const key of selectedKeys) if (!valueCache[key]) ids.push(key);
if (!ids.length) return;
Promise.resolve(props.fetchItems(ids)).then((items) => {
for (const item of items) valueCache[itemKey(item)] = item;
setRerender(!rerender);
});
}, [selectedKeys, multi]);
const handleInputChange = debounce((e?) => queryList(e ? e.target.value : ''), 300);
const toggleItem = (item: T, key = itemKey(item), preserve = false) => {
const shouldKeepOpen = multi && allowEmptyQuery && inputRef.current.value === '';
if (multi) {
const idx = selectedKeys.indexOf(key);
if (idx !== -1) {
setSelected((s) => {
const newSelected = [...s];
newSelected.splice(idx, 1);
return newSelected;
});
setSelectedKeys((s) => {
const newSelectedKeys = [...s];
newSelectedKeys.splice(idx, 1);
return newSelectedKeys;
});
} else {
setSelected((s) => [...s, item]);
setSelectedKeys((s) => [...s, key]);
}
if (!preserve) inputRef.current.value = '';
inputRef.current.focus();
} else {
setSelected([item]);
setSelectedKeys([key]);
inputRef.current.value = key;
}
dispatchChange();
if (!shouldKeepOpen) {
setItemList([]);
setCurrentItem(null);
}
};
const handleInputKeyDown = (e) => {
const { key, target } = e;
if (key === 'Escape') {
setItemList([]);
setCurrentItem(null);
return;
}
if (key === 'Enter' || key === ',') {
e.preventDefault();
if (currentItem !== null) {
toggleItem(itemList[currentItem]);
return;
}
if (freeSolo && target.value !== '') {
toggleItem(freeSoloConverter(target.value));
}
return;
}
if (key === 'Backspace') {
if (target.value.length) return;
if (selectedKeys.length) {
setSelected((s) => s.slice(0, -1));
setSelectedKeys((s) => s.slice(0, -1));
}
return;
}
if (key === 'ArrowUp') {
e.preventDefault();
if (itemList.length === 0) return;
const idx = (currentItem ?? 0) - 1;
const newIdx = idx < 0 ? itemList.length - 1 : idx;
setCurrentItem(newIdx);
listRef.current.children[newIdx].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
return;
}
if (key === 'ArrowDown') {
e.preventDefault();
if (itemList.length === 0) return;
const idx = (currentItem ?? itemList.length - 1) + 1;
const newIdx = idx >= itemList.length ? 0 : idx;
setCurrentItem(newIdx);
listRef.current.children[newIdx].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
// eslint-disable-next-line no-useless-return
return;
}
// TODO: handle other keys
};
useImperativeHandle(ref, () => ({
getSelectedItems: () => selected,
getSelectedItemKeys: () => [...selectedKeys, inputRef.current?.value].filter((v) => v?.trim().length),
getSelectedItemsAsString: () => [...selectedKeys, inputRef.current?.value].filter((v) => v?.trim().length).join(','),
setSelectedItems: (items) => {
setSelected(items);
setSelectedKeys(items.map((i) => itemKey(i)));
if (!multi && inputRef.current) inputRef.current.value = items.map((i) => itemKey(i)).join(',');
},
getQuery: () => inputRef.current?.value,
setQuery: (query) => {
if (inputRef.current) inputRef.current.value = query;
},
setSelectedKeys: (keys) => setSelectedKeys(keys),
triggerQuery: () => queryList(inputRef.current?.value),
closeList: () => {
setItemList([]);
setCurrentItem(null);
},
getValue: () => (multi ? selectedKeys.join(',') : (inputRef.current.value ?? '')),
getValueArray: () => (multi ? selected : [inputRef.current?.value].filter((i) => !!i)),
clear: () => {
setSelected([]);
setSelectedKeys([]);
if (inputRef.current) inputRef.current.value = '';
},
focus: () => {
setFocused(true);
inputRef.current?.focus();
},
}), [selected, selectedKeys, inputRef, multi]);
const move = (dragId: string, hoverId: string) => {
if (dragId === hoverId || !draggable) return;
const dragIndex = selectedKeys.indexOf(dragId);
const hoverIndex = selectedKeys.indexOf(hoverId);
setSelectedKeys((s) => {
const a = [...s];
a.splice(dragIndex, 1);
a.splice(hoverIndex, 0, s[dragIndex]);
return a;
});
};
return (
<div className="autocomplete-container" style={{ display: 'inline-block', width: '100%', marginBottom: '1rem' }}>
<div
className={focused ? 'autocomplete-wrapper focused' : 'autocomplete-wrapper'}
style={{ width, height }}
>
<DndProvider backend={HTML5Backend}>
{multi && selectedKeys.map((key) => {
const item = valueCache[key];
return (
<DraggableSelection type={draggableId} id={key} move={move} className="autocomplete-tag" key={item ? key : `draft-${key}`}>
<div>{item ? itemText(item) : key}</div>
<Icon name="close" onClick={() => toggleItem(item, key, true)} />
</DraggableSelection>
);
})}
<input
ref={inputRef}
autoComplete="off"
hidden={disabled}
onChange={(e) => {
dispatchChange();
handleInputChange(e);
}}
onFocus={() => {
if (allowEmptyQuery) handleInputChange();
setFocused(true);
}}
onPaste={async (e) => {
if (!multi) return;
const text = e.clipboardData.getData('text');
if (!text || (!text.includes(',') && !text.includes(','))) return;
e.preventDefault();
const ids = text.replace(/,/g, ',').split(',').filter((v) => v?.trim().length && !selectedKeys.includes(v));
if (!ids.length) return;
const fetched = await props.fetchItems(ids);
for (const item of fetched) valueCache[itemKey(item)] = item;
setSelected([...selected, ...fetched]);
setSelectedKeys([...selectedKeys, ...fetched.map((val) => itemKey(val))]);
}}
placeholder={props.placeholder}
onBlur={() => setFocused(false)}
onKeyDown={handleInputKeyDown}
defaultValue={multi ? '' : selectedKeys.join(',')}
/>
</DndProvider>
</div>
{disabled && (
<input
disabled
autoComplete="off"
value={disabledHint}
/>
)}
{focused && itemList.length > 0 && (
<ul ref={listRef} className="autocomplete-list" style={listStyle} onMouseDown={(e) => e.preventDefault()}>
{itemList.map((item, idx) => (
<li
key={itemKey(item)}
onClick={() => toggleItem(item)}
onMouseMove={() => setCurrentItem(idx)}
data-selected={selectedKeys.includes(itemKey(item))}
data-focus={idx === currentItem}
>
<div>{renderItem(item)}</div>
{selectedKeys.includes(itemKey(item)) && <Icon name="check" />}
</li>
))}
</ul>
)}
</div>
);
}) as (<T>(props: AutoCompleteProps<T> & { ref: React.Ref<AutoCompleteHandle<T>> }) => React.ReactElement) & React.FC;
AutoComplete.displayName = 'AutoComplete';
export default AutoComplete;
@@ -0,0 +1,48 @@
import { defaults } from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import AutoComplete from './AutoComplete';
type DefaultProps = {
_id: string;
name: string;
} & string;
const CustomSelectAutoComplete = React.forwardRef<any, any>((props, ref) => {
const def = defaults(props, {
width: '100%',
height: 'auto',
listStyle: {},
multi: false,
selectedKeys: [],
freeSolo: false,
freeSoloConverter: (input) => input,
});
return <AutoComplete<DefaultProps>
ref={ref as any}
fetchItems={(keys) => def.data.filter((i) => (i._id ? keys.includes(i._id) : keys.includes(i)))}
queryItems={(query) => def.data.filter((i) => (i.name || i).toString().toLowerCase().includes(query.toLowerCase()))}
itemText={(item) => `${item.name || item}`}
itemKey={(item) => `${item._id?.toString() || item.name || item}`}
renderItem={(item) => `${item.name || item}`}
allowEmptyQuery
{...def}
/>;
});
CustomSelectAutoComplete.propTypes = {
width: PropTypes.string,
height: PropTypes.string,
listStyle: PropTypes.object,
onChange: PropTypes.func.isRequired,
multi: PropTypes.bool,
selectedKeys: PropTypes.arrayOf(PropTypes.string),
allowEmptyQuery: PropTypes.bool,
freeSolo: PropTypes.bool,
freeSoloConverter: PropTypes.func,
data: PropTypes.arrayOf(PropTypes.any),
};
CustomSelectAutoComplete.displayName = 'CustomSelectAutoComplete';
export default CustomSelectAutoComplete;
@@ -0,0 +1,131 @@
@use '../common' as *;
.autocomplete-dummy {
display: none !important;
}
.autocomplete-wrapper {
border: 1px solid #d9d9d9;
padding: 1px;
display: flex;
flex-wrap: wrap;
overflow-y: auto;
@include form-styles();
background-color: $input-background-color;
outline: $input-outline;
transition: outline-color .2s, border-color .2s;
transition-timing-function: ease-out-cubic;
&:focus,
&.focused {
border-color: $input-focus-border-color;
outline: $input-focus-outline;
outline-offset: 0;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
// Overwrite, for IE compatibility
&:read-only {
outline: 0;
}
&:disabled {
opacity: 0.5;
}
&:hover {
border-color: #40a9ff;
}
input {
background-color: $input-background-color;
color: rgba(0, 0, 0, .85);
height: 30px;
box-sizing: border-box;
padding: 4px 6px;
width: 0;
min-width: 30px;
flex-grow: 1;
border: 0;
margin: 0;
outline: 0;
}
}
.autocomplete-tag {
display: flex;
align-items: center;
height: 24px;
margin: 2px;
line-height: 22px;
background-color: #fafafa;
border: 1px solid #e8e8e8;
border-radius: 2px;
box-sizing: content-box;
padding: 0 4px 0 10px;
outline: 0;
overflow: hidden;
&:focus {
border-color: #40a9ff;
background-color: #e6f7ff;
}
&>div {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&>span.bp5-icon>svg {
font-size: 12px;
cursor: pointer;
padding: 4px;
}
}
.autocomplete-list {
min-width: 300px;
margin: 2px 0 0;
padding: 0;
position: absolute;
list-style: none;
background-color: #fff;
overflow: auto;
max-height: 500px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 999;
li {
padding: 5px 12px;
display: flex;
&>div {
flex-grow: 1;
}
&>span.bp5-icon>svg {
color: transparent;
}
&[aria-selected='true'] {
background-color: #fafafa;
font-weight: 600;
&>span.bp5-icon>svg {
color: #1890ff;
}
}
&[data-focus='true'] {
background-color: #e6f7ff;
cursor: pointer;
&>span.bp5-icon>svg {
color: currentColor;
}
}
}
}
+202
View File
@@ -0,0 +1,202 @@
@use "sass:color";
@use "sass:list";
@use "sass:math";
@function toRem($value) {
$remValue: math.div($value, 16) + rem;
@return $remValue;
}
@function rem($values) {
$remValues: ();
@each $value in $values {
$remValues: list.append($remValues, toRem($value));
}
@return $remValues;
}
@function lighten($color, $amount) {
@return color.adjust($color, $lightness: $amount);
}
// layout
$grid-padding: 15px; // half gutter
// fonts
$primary-font-family: var(--font-family);
$header-font-family: var(--font-family);
$code-font-family: var(--code-font-family);
// primary palette
$default-color: #ddd;
$primary-color: #5f9fd6;
$secondary-color: #ed5f82;
$highlight-color: #df6589;
$code-color: #c92a2a;
$text-1-color: #555;
$text-2-color: lighten($text-1-color, 15%);
$text-3-color: lighten($text-1-color, 30%);
$header-1-color: #3d3d3d;
$header-2-color: lighten($header-1-color, 20%);
$blockquote-color: #999;
$page-bg-color: #edf0f2;
$content-bg-color: #fff;
$border-1-color: $default-color;
$border-2-color: lighten($default-color, 20%);
$immersive-primary-color: #ffef87;
$immersive-text-color: #ffffff;
$immersive-header-color: #ffffff;
$success-color: #25ad40;
$fail-color: #fb5555;
$progress-color: #f39800;
$invalid-color: #9fa0a0;
// sizes
$font-size: 16px; // main content
$font-size-secondary: 14px; // secondary content
$font-size-small: 13px; // additional informations, usually gray colored
$font-size-icon: 16px;
$font-size-title: 20px;
// components
$section-gap-v: 15px;
$section-gap-h: $grid-padding;
$section-list-v: 25px;
$section-margin: 30px;
$section-shadow: 0 rem(6px) rem(22px) rgba(#AFC2C9, 0.5);
$section-bg-color: $content-bg-color;
$section-title-color: $header-2-color;
$section-toolbtn-color: #AAA;
$section-toolbtn-color-hover: #444;
$autocomplete-empty-row-color: #AAA;
$userselect-uid-color: #BBB;
$editor-border-color: $border-1-color;
$datepicker-shadow: 0 rem(2px) rem(7px) rgba(#000, 0.3);
$datapicker-list-item-bg-color-hover: #F0F0F0;
$dialog-layer-bg-color: rgba(#F0F0F0, 0.6);
$dialog-bg-color: $content-bg-color;
$dialog-border-color: $border-1-color;
$dialog-shadow: 0 rem(2px) rem(10px) rgba(#000, 0.2);
$dialog-title-color: $header-2-color;
$comment-placeholder-color: #AAA;
$comment-op-color: lighten($text-1-color, 30%);
$comment-op-link-color: lighten($text-1-color, 50%);
$menu-item-bg-color-hover: #F4F4F4;
$menu-drop-shadow: 0 rem(2px) rem(7px) rgba(#000, 0.3);
$menu-drop-bg-color: $content-bg-color;
$menu-drop-triangle-size: 4px;
$menu-drop-triangle-shadow: rem(-2px) rem(-2px) rem(2px) rgba(#000, 0.15);
$nav-item-height: 45px;
$nav-item-hover-color: $primary-color;
$nav-item-active-color: $secondary-color;
$nav-item-active-immersive-color: #9AE3F3;
$nav-item-round-height: 32px;
$nav-item-round-color: $primary-color;
$nav-item-round-border: 2px;
// $nav-logo-height: 32px;
// $nav-logo-height-mobile: 23px;
$header-layer-height: 100px;
$header-bg-width: 1920px;
$header-bg-height: 320px;
$header-bg-height-mini: $header-bg-height - $header-layer-height;
//$header-logo-width = 280px
//$header-logo-height = 150px
//$header-logo-width-real = 280px
//$header-logo-height-real = 150px
$header-logo-width: 252px;
$header-logo-height: 150px;
$header-logo-width-real: 381px;
$header-logo-height-real: 201px;
$header-logo-domain-lh: 60px;
$header-logo-system-lh: 30px;
$footer-bg-color: $content-bg-color;
$input-focus-color: #C1E0FF;
$form-control-height: 35px;
$compact-control-height: 28px;
$input-margin: 0 0 1rem;
$input-multi-line-margin: 0 0.3rem 0.15rem 0;
$input-image-radio-margin: 0 0.3rem 0.3rem 0;
$input-background-color: $content-bg-color;
$input-border: 1px solid $border-1-color;
$input-outline: 2px solid transparent;
$input-focus-border-color: $primary-color;
$input-focus-outline: 2px solid $input-focus-color;
$label-color: lighten($text-1-color, 10%);
$help-text-color: lighten($text-1-color, 30%);
$table-border-color: $border-2-color;
$table-alternate-bg-color: #F4F4F4;
$table-header-bg-color: rgba(#FFF, 0.95);
$table-header-shadow: rem(0 3px 2px) rgba(#000, 0.03);
$table-header-shadow-floating: rem(0 2px 5px) rgba(#000, 0.2);
$table-padding-h: 10px;
$table-lh: 26px;
$toolbar-bg: #f2f6f7;
$toolbar-fore: #8395a1;
$toolbar-fore-hover: #5a6b75;
$toolbar-bg-hover: #e3e7e8;
$toolbar-fore-selected: #FFF;
$toolbar-bg-selected: $secondary-color;
$toolbar-disabled-opacity: 0.3;
$supplementary-color: lighten($text-1-color, 50%);
$supplementary-border-color: lighten($supplementary-color, 62%);
$supplementary-link-color: lighten($text-1-color, 40%);
$pager-border-color: $table-border-color;
$nprogress-color: $primary-color;
$syntax-hl-bg-color: #F8F8F8;
// // Notification
// $notification-color = {
// 'success': #9BDB7B,
// 'info': #78D6F4,
// 'warn': #FCD279,
// 'error': #FD848D,
// }
// $notification-icon = {
// 'success': $icon-check--circle,
// 'info': $icon-info--circle,
// 'warn': $icon-info--circle,
// 'error': $icon-close--circle,
// }
@mixin form-styles() {
appearance: none;
display: block;
width: 100%;
font-size: rem($font-size-secondary);
margin: $input-margin;
height: rem($form-control-height);
line-height: 1.2;
padding: rem(5px);
border: $input-border;
&.inline {
display: inline-block;
width: auto;
}
}
.fullscreen-content {
position: absolute;
left: 0;
top: $nav-item-height;
width: 100%;
bottom: 0;
}
+6
View File
@@ -0,0 +1,6 @@
export { default as ComponentsProvider } from './provider';
export { default as ConfigEditor } from './ConfigEditor';
export { default as Icon } from './Icon';
export { default as Notification } from './Notification';
export { default as AutoComplete, type AutoCompleteHandle, type AutoCompleteProps } from './autocomplete/AutoComplete';
export { default as CustomSelectAutoComplete } from './autocomplete/CustomSelectAutoComplete';
+33
View File
@@ -0,0 +1,33 @@
import React from 'react';
import { ToastContainer } from 'react-toastify';
interface C {
i18n: (key: string, ...args: any[]) => string;
theme: 'light' | 'dark';
codeFontFamily: string;
}
// eslint-disable-next-line
export const ComponentsContext = React.createContext<C>({
i18n: (key) => key,
theme: 'light',
codeFontFamily: 'monospace',
});
export default function ComponentsProvider(props: { children: React.ReactNode } & C) {
return <ComponentsContext.Provider value={props}>
<ToastContainer
position="bottom-left"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick={false}
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme={props.theme}
/>
{props.children}
</ComponentsContext.Provider>;
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@hydrooj/components",
"version": "1.0.0-alpha.0",
"main": "./frontend/index.ts",
"dependencies": {
"diff": "^7.0.0",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-toastify": "^11.0.5",
"schemastery-react": "^0.1.2"
},
"devDependencies": {
"@types/diff": "^7.0.2",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.16",
"sass": "^1.86.0"
}
}