@halooj/components 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,187 @@
1
+ import 'schemastery-react/lib/schemastery-react.css';
2
+
3
+ import { Allotment } from 'allotment';
4
+ import { diffLines } from 'diff';
5
+ import yaml from 'js-yaml';
6
+ import React from 'react';
7
+ import { createSchemasteryReact } from 'schemastery-react';
8
+ import { ComponentsContext } from './provider';
9
+
10
+ const locales = {
11
+ zh: 'zh-CN',
12
+ en: 'en-US',
13
+ };
14
+
15
+ function MonacoContainer({
16
+ config, setValue, setError, monaco, schema, editorCallback, size, dark,
17
+ }) {
18
+ const [editor, setEditor] = React.useState<any>(null);
19
+ const [model, setModel] = React.useState<any>(null);
20
+ const [initialized, setInitialized] = React.useState(false);
21
+ const { codeFontFamily } = React.useContext(ComponentsContext);
22
+
23
+ React.useEffect(() => {
24
+ if (!monaco) return;
25
+ const interval = setInterval(() => {
26
+ monaco.editor.remeasureFonts();
27
+ }, 1000);
28
+ return () => clearInterval(interval); // eslint-disable-line consistent-return
29
+ }, [monaco]);
30
+
31
+ React.useEffect(() => {
32
+ const listener = () => {
33
+ if (!editor) return;
34
+ editor.layout();
35
+ };
36
+ window.addEventListener('resize', listener);
37
+ return () => window.removeEventListener('resize', listener);
38
+ }, [editor]);
39
+ React.useEffect(() => {
40
+ if (!editor) return;
41
+ editor.layout();
42
+ }, [size]);
43
+ React.useEffect(() => {
44
+ if (!editor || !model) return;
45
+ const decoration = editor.createDecorationsCollection();
46
+ const updateDecoration = () => {
47
+ const found = model.findMatches("'[hidden]'", true, false, true, '', true).map((i) => i.range);
48
+ decoration.set(found.map((range) => ({ range, options: { inlineClassName: 'decoration-hide' } })));
49
+ };
50
+ updateDecoration();
51
+ const disposable = editor.onDidChangeModelContent(() => {
52
+ const val = editor.getValue({ lineEnding: '\n', preserveBOM: false });
53
+ updateDecoration();
54
+ try {
55
+ const loaded = yaml.load(val);
56
+ schema(loaded);
57
+ setValue(val);
58
+ setError('');
59
+ } catch (err) {
60
+ setError(err.message);
61
+ }
62
+ });
63
+ return () => disposable.dispose(); // eslint-disable-line
64
+ }, [editor, model, setValue, setError, schema]);
65
+ const initializeEditor = React.useCallback((element) => {
66
+ if (!element || initialized) return;
67
+ setInitialized(true);
68
+ // eslint-disable-next-line
69
+ const model = monaco.editor.createModel(config, 'yaml', monaco.Uri.parse('hydro://system/setting.yaml'));
70
+ setModel(model);
71
+ const e = monaco.editor.create(element, {
72
+ theme: dark ? 'vs-dark' : 'vs-light',
73
+ lineNumbers: 'off',
74
+ glyphMargin: true,
75
+ lightbulb: { enabled: monaco.editor.ShowLightbulbIconMode.On },
76
+ model,
77
+ minimap: { enabled: false },
78
+ hideCursorInOverviewRuler: true,
79
+ overviewRulerLanes: 0,
80
+ overviewRulerBorder: false,
81
+ fontFamily: codeFontFamily,
82
+ fontLigatures: '',
83
+ unicodeHighlight: {
84
+ ambiguousCharacters: true,
85
+ },
86
+ });
87
+ editorCallback(e, model, element);
88
+ setEditor(e);
89
+ }, [config, monaco, editorCallback, codeFontFamily, initialized]);
90
+ React.useEffect(() => {
91
+ if (!editor) return;
92
+ const current = editor.getValue({ lineEnding: '\n', preserveBOM: false });
93
+ const diff = diffLines(current, config);
94
+ const ops: { range: any, text: string }[] = [];
95
+ let cursor = 1;
96
+ for (const line of diff) {
97
+ if (line.added) {
98
+ let range = model.getFullModelRange();
99
+ range = range.setStartPosition(cursor, 0);
100
+ range = range.setEndPosition(cursor, 0);
101
+ ops.push({ range, text: line.value });
102
+ } else if (line.removed) {
103
+ let range = model.getFullModelRange();
104
+ range = range.setStartPosition(cursor, 0);
105
+ cursor += line.count;
106
+ range = range.setEndPosition(cursor, 0);
107
+ ops.push({ range, text: '' });
108
+ } else cursor += line.count;
109
+ }
110
+ model.pushEditOperations([], ops, undefined);
111
+ }, [editor, config]);
112
+ return <div ref={initializeEditor} style={{ width: '100%', height: '80vh' }} />;
113
+ }
114
+
115
+ export default function ConfigEditor({
116
+ schema, config, monaco, Markdown, onSave, registerAction, sidebar, dynamic,
117
+ }) {
118
+ const getValue = () => {
119
+ try {
120
+ return yaml.load(config);
121
+ } catch (e) {
122
+ return {};
123
+ }
124
+ };
125
+ const [value, setValue] = React.useState(getValue);
126
+ const [info, setInfo] = React.useState('');
127
+ const [stringConfig, setStringConfig] = React.useState(config);
128
+ const initial = React.useMemo(getValue, []);
129
+ const { i18n } = React.useContext(ComponentsContext);
130
+
131
+ const Form = React.useMemo(() => createSchemasteryReact({
132
+ locale: locales[i18n('__id')] || 'en-US',
133
+ Markdown,
134
+ }), []);
135
+
136
+ const updateFromForm = React.useCallback((v) => {
137
+ const newDump = yaml.dump(v);
138
+ if (newDump === stringConfig) return;
139
+ setStringConfig(newDump);
140
+ setValue(v);
141
+ }, [stringConfig]);
142
+ const updateFromMonaco = React.useCallback((v) => {
143
+ if (v === stringConfig) return;
144
+ setStringConfig(v);
145
+ setValue(yaml.load(v));
146
+ }, [stringConfig]);
147
+
148
+ // FIXME: Otherwise first form change will be ignored
149
+ React.useEffect(() => {
150
+ setTimeout(() => {
151
+ updateFromMonaco(stringConfig === '{}' ? 'dummy: 1' : `${stringConfig}\n\ndummy: 1`);
152
+ setTimeout(() => {
153
+ setStringConfig(stringConfig || '\n');
154
+ setValue(yaml.load(stringConfig));
155
+ }, 300);
156
+ }, 300);
157
+ }, []);
158
+
159
+ const [size, setSize] = React.useState([50, 50]);
160
+
161
+ return (<div className="fullscreen-content" style={{ zIndex: 10 }}>
162
+ <Allotment onChange={setSize}>
163
+ <Allotment.Pane>
164
+ <MonacoContainer
165
+ editorCallback={registerAction}
166
+ schema={schema}
167
+ monaco={monaco}
168
+ config={stringConfig}
169
+ setValue={updateFromMonaco}
170
+ setError={setInfo}
171
+ size={size[0]}
172
+ dark={document.documentElement.className.includes('theme--dark')}
173
+ />
174
+ <pre className="help-text">{info}</pre>
175
+ <button onClick={() => onSave(stringConfig)} className="rounded primary button">{i18n('Save All Changes')}</button>
176
+ </Allotment.Pane>
177
+ <Allotment.Pane>
178
+ <div style={{
179
+ overflowY: 'scroll', top: 0, bottom: 0, left: 0, right: 0, position: 'absolute', marginLeft: 10,
180
+ }}>
181
+ <Form schema={schema} initial={initial} value={value} onChange={updateFromForm} dynamic={dynamic} />
182
+ </div>
183
+ </Allotment.Pane>
184
+ {sidebar && <Allotment.Pane maxSize={220}>{sidebar}</Allotment.Pane>}
185
+ </Allotment>
186
+ </div>);
187
+ }
@@ -0,0 +1,14 @@
1
+ import { omit } from 'lodash';
2
+ import React from 'react';
3
+
4
+ export default function DomComponent(props: React.HTMLAttributes<HTMLDivElement> & { childDom: HTMLElement }) {
5
+ const [dom, setDom] = React.useState<HTMLElement | null>(null);
6
+ React.useEffect(() => {
7
+ if (!dom) return;
8
+ dom.appendChild(props.childDom);
9
+ return () => { // eslint-disable-line consistent-return
10
+ dom.removeChild(props.childDom);
11
+ };
12
+ }, [dom]);
13
+ return <div {...omit(props, 'childDom')} ref={setDom}></div>;
14
+ }
@@ -0,0 +1,20 @@
1
+ import classNames from 'classnames';
2
+ import PropTypes from 'prop-types';
3
+ import React from 'react';
4
+
5
+ export default function IconComponent(props: { name: string, className?: string } & React.HTMLAttributes<HTMLSpanElement>) {
6
+ const {
7
+ name,
8
+ className,
9
+ ...rest
10
+ } = props;
11
+ const cn = classNames(className, `icon icon-${name}`);
12
+ return (
13
+ <span {...rest} className={cn} />
14
+ );
15
+ }
16
+
17
+ IconComponent.propTypes = {
18
+ name: PropTypes.string.isRequired,
19
+ className: PropTypes.string,
20
+ };
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+
3
+ export function Row({ children }: { children: React.ReactNode }) {
4
+ return <div className="row">
5
+ {children}
6
+ </div>;
7
+ }
8
+
9
+ export function Column({ children, md }: { children: React.ReactNode, md?: number }) {
10
+ return <div className={`columns medium-${md || 12}`}>
11
+ {children}
12
+ </div>;
13
+ }
@@ -0,0 +1,26 @@
1
+ import { toast } from 'react-toastify';
2
+
3
+ export default class Notification {
4
+ type: string;
5
+ action: any;
6
+ $dom: JQuery<HTMLElement>;
7
+ $n: JQuery<HTMLElement>;
8
+ duration: number;
9
+ autoHideTimer?: NodeJS.Timeout;
10
+
11
+ static async success(message: string, duration?: number) {
12
+ return toast.success(message, { autoClose: duration });
13
+ }
14
+
15
+ static async info(message: string, duration?: number) {
16
+ return toast.info(message, { autoClose: duration });
17
+ }
18
+
19
+ static async warn(message: string, duration?: number) {
20
+ return toast.warning(message, { autoClose: duration });
21
+ }
22
+
23
+ static async error(message: string, duration?: number) {
24
+ return toast.error(message, { autoClose: duration });
25
+ }
26
+ }
@@ -0,0 +1,344 @@
1
+ import './autocomplete.scss';
2
+
3
+ import { debounce, uniqueId } from 'lodash';
4
+ import React, {
5
+ forwardRef, useEffect,
6
+ useImperativeHandle, useRef, useState,
7
+ } from 'react';
8
+ import { DndProvider, useDrag, useDrop } from 'react-dnd';
9
+ import { HTML5Backend } from 'react-dnd-html5-backend';
10
+ import Icon from '../Icon';
11
+
12
+ export interface AutoCompleteProps<Item> {
13
+ width?: string;
14
+ /**
15
+ * if you need fix height, set to at least "30px"
16
+ * for Hydro, no less then "34px" can be better
17
+ */
18
+ height?: string;
19
+ disabled?: boolean;
20
+ placeholder?: string;
21
+ disabledHint?: string;
22
+ listStyle?: React.CSSProperties;
23
+ cacheKey?: string;
24
+ renderItem?: (item: Item) => any;
25
+ queryItems?: (query: string) => Promise<Item[]> | Item[];
26
+ fetchItems?: (ids: string[]) => Promise<Item[]> | Item[];
27
+ itemText?: (item: Item) => string;
28
+ itemKey?: (item: Item) => string;
29
+ onChange?: (value: string) => any;
30
+ multi?: boolean;
31
+ draggable?: boolean;
32
+ selectedKeys?: string[];
33
+ allowEmptyQuery?: boolean;
34
+ freeSolo?: boolean;
35
+ freeSoloConverter?: (value: string) => string;
36
+ }
37
+
38
+ export interface AutoCompleteHandle<Item> {
39
+ getSelectedItems: () => Item[];
40
+ getSelectedItemKeys: () => string[];
41
+ setSelectedItems: (items: Item[]) => void;
42
+ getQuery: () => string;
43
+ setQuery: (query: string) => void;
44
+ setSelectedKeys: (keys: string[]) => void;
45
+ triggerQuery: () => any;
46
+ closeList: () => void;
47
+ getValue: () => string;
48
+ clear: () => void;
49
+ focus: () => void;
50
+ }
51
+
52
+ const superCache = {};
53
+
54
+ function DraggableSelection({
55
+ type, id, move, children, ...props
56
+ }) {
57
+ const ref = useRef<HTMLDivElement>(null);
58
+ const [isDragging, drag] = useDrag<{ id: string }, any, boolean>(() => ({
59
+ type,
60
+ item: { id },
61
+ collect: (m) => m.isDragging(),
62
+ }));
63
+ const [, drop] = useDrop({
64
+ accept: type,
65
+ hover: (item: { id: string }) => {
66
+ if (!ref.current) return;
67
+ move(item.id, id);
68
+ },
69
+ });
70
+ drag(drop(ref));
71
+ return (
72
+ <div ref={ref} {...props} style={{ opacity: isDragging ? 0.2 : 1 }}>
73
+ {children}
74
+ </div>
75
+ );
76
+ }
77
+
78
+ // eslint-disable-next-line prefer-arrow-callback
79
+ const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, ref: React.Ref<AutoCompleteHandle<T>>) {
80
+ const {
81
+ multi = false, width = '100%', height = 'auto',
82
+ freeSolo = false, allowEmptyQuery = false, listStyle = {},
83
+ disabled = false, disabledHint = '', draggable = multi,
84
+ } = props;
85
+ const queryItems = props.queryItems ?? (() => []);
86
+ const renderItem = props.renderItem ?? ((item) => item);
87
+ const itemText = props.itemText ?? ((item) => item.toString());
88
+ const itemKey = props.itemKey ?? itemText;
89
+ const onChange = props.onChange ?? (() => { });
90
+ const freeSoloConverter = freeSolo ? props.freeSoloConverter ?? ((i) => i) : (i) => i;
91
+
92
+ const [focused, setFocused] = useState(false); // is focused
93
+ const [selectedKeys, setSelectedKeys] = useState(props.selectedKeys || []); // keys of selected items
94
+ const [itemList, setItemList] = useState([]); // items list
95
+ const [currentItem, setCurrentItem] = useState(null); // index of current item (in item list)
96
+ const [rerender, setRerender] = useState(false);
97
+ const [draggableId] = useState(() => uniqueId());
98
+
99
+ const inputRef = useRef<HTMLInputElement>(null);
100
+ const listRef = useRef<HTMLUListElement>(null);
101
+
102
+ let [queryCache, valueCache] = [useRef({}).current, useRef({}).current];
103
+ if (props.cacheKey) {
104
+ superCache[props.cacheKey] ||= { query: {}, value: {} };
105
+ queryCache = superCache[props.cacheKey].query;
106
+ valueCache = superCache[props.cacheKey].value;
107
+ }
108
+
109
+ const queryList = async (query) => {
110
+ if (!query && !allowEmptyQuery) {
111
+ setItemList([]);
112
+ setCurrentItem(null);
113
+ return;
114
+ }
115
+ try {
116
+ queryCache[query] ||= await queryItems(query);
117
+ for (const item of queryCache[query]) valueCache[itemKey(item)] = item;
118
+ setItemList(queryCache[query]);
119
+ setCurrentItem((!freeSolo && queryCache[query].length) ? 0 : null);
120
+ } catch (e) {
121
+ console.error('Failed to query items', e);
122
+ setItemList([]);
123
+ setCurrentItem(null);
124
+ }
125
+ };
126
+
127
+ useEffect(() => {
128
+ setSelectedKeys(props.selectedKeys || []);
129
+ }, [JSON.stringify(props.selectedKeys)]);
130
+ const dispatchChange = () => {
131
+ if (!multi) onChange(inputRef.current?.value);
132
+ else onChange(selectedKeys.filter((v) => v?.trim().length).join(','));
133
+ };
134
+
135
+ useEffect(() => {
136
+ dispatchChange();
137
+ if (!multi) return; // Load pre-selected items only in multi mode
138
+ const ids = [];
139
+ for (const key of selectedKeys) if (!valueCache[key]) ids.push(key);
140
+ if (!ids.length) return;
141
+ Promise.resolve(props.fetchItems(ids)).then((items) => {
142
+ for (const item of items) valueCache[itemKey(item)] = item;
143
+ setRerender(!rerender);
144
+ }).catch((e) => {
145
+ console.error('Failed to fetch items', e);
146
+ });
147
+ }, [selectedKeys, multi]);
148
+
149
+ const handleInputChange = debounce((e?) => queryList(e ? e.target.value : ''), 300);
150
+
151
+ const toggleItem = (item: T, key = itemKey(item), preserve = false) => {
152
+ const shouldKeepOpen = multi && allowEmptyQuery && inputRef.current.value === '';
153
+ if (multi) {
154
+ const idx = selectedKeys.indexOf(key);
155
+ if (idx !== -1) {
156
+ setSelectedKeys((s) => {
157
+ const newSelectedKeys = [...s];
158
+ newSelectedKeys.splice(idx, 1);
159
+ return newSelectedKeys;
160
+ });
161
+ } else {
162
+ setSelectedKeys((s) => [...s, key]);
163
+ }
164
+ if (!preserve) inputRef.current.value = '';
165
+ inputRef.current.focus();
166
+ } else {
167
+ setSelectedKeys([key]);
168
+ inputRef.current.value = key;
169
+ }
170
+ if (!shouldKeepOpen) {
171
+ setItemList([]);
172
+ setCurrentItem(null);
173
+ }
174
+ };
175
+
176
+ const handleInputKeyDown = (e) => {
177
+ const { key, target } = e;
178
+ if (key === 'Escape') {
179
+ setItemList([]);
180
+ setCurrentItem(null);
181
+ return;
182
+ }
183
+ if (key === 'Enter' || key === ',') {
184
+ e.preventDefault();
185
+ if (currentItem !== null) {
186
+ toggleItem(itemList[currentItem]);
187
+ return;
188
+ }
189
+ if (freeSolo && target.value !== '') {
190
+ toggleItem(freeSoloConverter(target.value));
191
+ }
192
+ return;
193
+ }
194
+ if (key === 'Backspace') {
195
+ if (target.value.length) return;
196
+ if (selectedKeys.length) {
197
+ setSelectedKeys((s) => s.slice(0, -1));
198
+ }
199
+ return;
200
+ }
201
+ if (key === 'ArrowUp') {
202
+ e.preventDefault();
203
+ if (itemList.length === 0) return;
204
+ const idx = (currentItem ?? 0) - 1;
205
+ const newIdx = idx < 0 ? itemList.length - 1 : idx;
206
+ setCurrentItem(newIdx);
207
+ listRef.current.children[newIdx].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
208
+ return;
209
+ }
210
+ if (key === 'ArrowDown') {
211
+ e.preventDefault();
212
+ if (itemList.length === 0) return;
213
+ const idx = (currentItem ?? itemList.length - 1) + 1;
214
+ const newIdx = idx >= itemList.length ? 0 : idx;
215
+ setCurrentItem(newIdx);
216
+ listRef.current.children[newIdx].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
217
+ // eslint-disable-next-line no-useless-return
218
+ return;
219
+ }
220
+ // TODO: handle other keys
221
+ };
222
+
223
+ useImperativeHandle(ref, () => ({
224
+ getSelectedItems: () => selectedKeys.map((key) => valueCache[key]),
225
+ getSelectedItemKeys: () => [...selectedKeys, inputRef.current?.value].filter((v) => v?.trim().length),
226
+ setSelectedItems: (items) => {
227
+ setSelectedKeys(items.map((i) => itemKey(i)));
228
+ if (!multi && inputRef.current) inputRef.current.value = items.map((i) => itemKey(i)).join(',');
229
+ },
230
+ setSelectedKeys,
231
+ getQuery: () => inputRef.current?.value,
232
+ setQuery: (query) => {
233
+ if (inputRef.current) inputRef.current.value = query;
234
+ },
235
+ triggerQuery: () => queryList(inputRef.current?.value),
236
+ closeList: () => {
237
+ setItemList([]);
238
+ setCurrentItem(null);
239
+ },
240
+ getValue: () => (multi ? selectedKeys.join(',') : (inputRef.current.value ?? '')),
241
+ clear: () => {
242
+ setSelectedKeys([]);
243
+ if (inputRef.current) inputRef.current.value = '';
244
+ },
245
+ focus: () => {
246
+ setFocused(true);
247
+ inputRef.current?.focus();
248
+ },
249
+ }), [selectedKeys, inputRef, multi]);
250
+
251
+ const move = (dragId: string, hoverId: string) => {
252
+ if (dragId === hoverId || !draggable) return;
253
+ const dragIndex = selectedKeys.indexOf(dragId);
254
+ const hoverIndex = selectedKeys.indexOf(hoverId);
255
+ setSelectedKeys((s) => {
256
+ const a = [...s];
257
+ a.splice(dragIndex, 1);
258
+ a.splice(hoverIndex, 0, s[dragIndex]);
259
+ return a;
260
+ });
261
+ };
262
+
263
+ return (
264
+ <div className="autocomplete-container" style={{ display: 'inline-block', width: '100%', marginBottom: '1rem' }}>
265
+ <div
266
+ className={focused ? 'autocomplete-wrapper focused' : 'autocomplete-wrapper'}
267
+ style={{ width, height }}
268
+ >
269
+ <DndProvider backend={HTML5Backend} context={window}>
270
+ {multi && selectedKeys.map((key) => {
271
+ const item = valueCache[key];
272
+ return (
273
+ <DraggableSelection type={draggableId} id={key} move={move} className="autocomplete-tag" key={item ? key : `draft-${key}`}>
274
+ <div>{item ? itemText(item) : key}</div>
275
+ <Icon name="close" onClick={() => toggleItem(item, key, true)} />
276
+ </DraggableSelection>
277
+ );
278
+ })}
279
+ <input
280
+ ref={inputRef}
281
+ autoComplete="off"
282
+ hidden={disabled}
283
+ onChange={(e) => {
284
+ dispatchChange();
285
+ handleInputChange(e);
286
+ }}
287
+ onFocus={() => {
288
+ if (allowEmptyQuery) handleInputChange();
289
+ setFocused(true);
290
+ }}
291
+ onPaste={async (e) => {
292
+ if (!multi) return;
293
+ const text = e.clipboardData.getData('text');
294
+ if (!text || (!text.includes(',') && !text.includes(','))) return;
295
+ e.preventDefault();
296
+ const ids = text.replace(/,/g, ',').split(',').filter((v) => v?.trim().length && !selectedKeys.includes(v));
297
+ if (!ids.length) return;
298
+ try {
299
+ const fetched = await props.fetchItems(ids);
300
+ for (const item of fetched) valueCache[itemKey(item)] = item;
301
+ setSelectedKeys([...selectedKeys, ...fetched.map((val) => itemKey(val))]);
302
+ } catch (err) {
303
+ console.error('Failed to fetch items on paste', err);
304
+ }
305
+ }}
306
+ placeholder={props.placeholder}
307
+ onBlur={() => setFocused(false)}
308
+ onKeyDown={handleInputKeyDown}
309
+ defaultValue={multi ? '' : selectedKeys.join(',')}
310
+ />
311
+ </DndProvider>
312
+ </div>
313
+ {disabled && (
314
+ <input
315
+ disabled
316
+ autoComplete="off"
317
+ value={disabledHint}
318
+ />
319
+ )}
320
+ {focused && itemList.length > 0 && (
321
+ <ul ref={listRef} className="autocomplete-list" style={listStyle} onMouseDown={(e) => e.preventDefault()}>
322
+ {itemList.map((item, idx) => {
323
+ const inner = renderItem(item);
324
+ if (!inner) return null;
325
+ return <li
326
+ key={itemKey(item)}
327
+ onClick={() => toggleItem(item)}
328
+ onMouseMove={() => setCurrentItem(idx)}
329
+ data-selected={selectedKeys.includes(itemKey(item))}
330
+ data-focus={idx === currentItem}
331
+ >
332
+ <div>{inner}</div>
333
+ {selectedKeys.includes(itemKey(item)) && <Icon name="check" />}
334
+ </li>;
335
+ })}
336
+ </ul>
337
+ )}
338
+ </div>
339
+ );
340
+ }) as (<T>(props: AutoCompleteProps<T> & { ref: React.Ref<AutoCompleteHandle<T>> }) => React.ReactElement) & React.FC;
341
+
342
+ AutoComplete.displayName = 'AutoComplete';
343
+
344
+ export default AutoComplete;
@@ -0,0 +1,48 @@
1
+ import { defaults } from 'lodash';
2
+ import PropTypes from 'prop-types';
3
+ import React from 'react';
4
+ import AutoComplete from './AutoComplete';
5
+
6
+ type DefaultProps = {
7
+ _id: string;
8
+ name: string;
9
+ } & string;
10
+
11
+ const CustomSelectAutoComplete = React.forwardRef<any, any>((props, ref) => {
12
+ const def = defaults(props, {
13
+ width: '100%',
14
+ height: 'auto',
15
+ listStyle: {},
16
+ multi: false,
17
+ selectedKeys: [],
18
+ freeSolo: false,
19
+ freeSoloConverter: (input) => input,
20
+ });
21
+ return <AutoComplete<DefaultProps>
22
+ ref={ref as any}
23
+ fetchItems={(keys) => def.data.filter((i) => (i._id ? keys.includes(i._id) : keys.includes(i)))}
24
+ queryItems={(query) => def.data.filter((i) => (i.name || i).toString().toLowerCase().includes(query.toLowerCase()))}
25
+ itemText={(item) => `${item.name || item}`}
26
+ itemKey={(item) => `${item._id?.toString() || item.name || item}`}
27
+ renderItem={(item) => `${item.name || item}`}
28
+ allowEmptyQuery
29
+ {...def}
30
+ />;
31
+ });
32
+
33
+ CustomSelectAutoComplete.propTypes = {
34
+ width: PropTypes.string,
35
+ height: PropTypes.string,
36
+ listStyle: PropTypes.object,
37
+ onChange: PropTypes.func.isRequired,
38
+ multi: PropTypes.bool,
39
+ selectedKeys: PropTypes.arrayOf(PropTypes.string),
40
+ allowEmptyQuery: PropTypes.bool,
41
+ freeSolo: PropTypes.bool,
42
+ freeSoloConverter: PropTypes.func,
43
+ data: PropTypes.arrayOf(PropTypes.any),
44
+ };
45
+
46
+ CustomSelectAutoComplete.displayName = 'CustomSelectAutoComplete';
47
+
48
+ export default CustomSelectAutoComplete;
@@ -0,0 +1,111 @@
1
+ @use '../common' as *;
2
+
3
+ .autocomplete-dummy {
4
+ display: none !important;
5
+ }
6
+
7
+ .autocomplete-wrapper {
8
+ border: 1px solid #d9d9d9;
9
+ padding: 1px;
10
+ display: flex;
11
+ flex-wrap: wrap;
12
+ overflow-y: auto;
13
+
14
+ background-color: $input-background-color;
15
+ outline: $input-outline;
16
+ transition: outline-color .2s, border-color .2s;
17
+ transition-timing-function: ease-out-cubic;
18
+
19
+ &:focus,
20
+ &.focused {
21
+ border-color: $input-focus-border-color;
22
+ outline: $input-focus-outline;
23
+ outline-offset: 0;
24
+ box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
25
+ }
26
+
27
+ // Overwrite, for IE compatibility
28
+ &:read-only {
29
+ outline: 0;
30
+ }
31
+
32
+ &:disabled {
33
+ opacity: 0.5;
34
+ }
35
+
36
+ &:hover {
37
+ border-color: #40a9ff;
38
+ }
39
+
40
+ input {
41
+ background-color: $input-background-color;
42
+ color: rgba(0, 0, 0, .85);
43
+ height: 30px;
44
+ box-sizing: border-box;
45
+ padding: 4px 6px;
46
+ min-width: 30px;
47
+ flex-grow: 1;
48
+ border: 0;
49
+ margin: 0;
50
+ outline: 0;
51
+ }
52
+ }
53
+
54
+ .autocomplete-tag {
55
+ display: flex;
56
+ align-items: center;
57
+ height: 24px;
58
+ margin: 2px;
59
+ line-height: 22px;
60
+ background-color: #fafafa;
61
+ border: 1px solid #e8e8e8;
62
+ border-radius: 2px;
63
+ box-sizing: content-box;
64
+ padding: 0 4px 0 10px;
65
+ outline: 0;
66
+ overflow: hidden;
67
+
68
+ &:focus {
69
+ border-color: #40a9ff;
70
+ background-color: #e6f7ff;
71
+ }
72
+
73
+ &>div {
74
+ overflow: hidden;
75
+ white-space: nowrap;
76
+ text-overflow: ellipsis;
77
+ }
78
+ }
79
+
80
+ .autocomplete-list {
81
+ min-width: 300px;
82
+ margin: 2px 0 0;
83
+ padding: 0;
84
+ position: absolute;
85
+ list-style: none;
86
+ background-color: #fff;
87
+ overflow: auto;
88
+ max-height: 500px;
89
+ border-radius: 4px;
90
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
91
+ z-index: 999;
92
+
93
+ li {
94
+ padding: 5px 12px;
95
+ display: flex;
96
+
97
+ &>div {
98
+ flex-grow: 1;
99
+ }
100
+
101
+ &[aria-selected='true'] {
102
+ background-color: #fafafa;
103
+ font-weight: 600;
104
+ }
105
+
106
+ &[data-focus='true'] {
107
+ background-color: #e6f7ff;
108
+ cursor: pointer;
109
+ }
110
+ }
111
+ }
@@ -0,0 +1,202 @@
1
+ @use "sass:color";
2
+ @use "sass:list";
3
+ @use "sass:math";
4
+
5
+ @function toRem($value) {
6
+ $remValue: calc($value / 16px) * 1rem;
7
+ @return $remValue;
8
+ }
9
+ @function rem($values) {
10
+ $remValues: ();
11
+ @each $value in $values {
12
+ $remValues: list.append($remValues, toRem($value));
13
+ }
14
+ @return $remValues;
15
+ }
16
+ @function lighten($color, $amount) {
17
+ @return color.adjust($color, $lightness: $amount);
18
+ }
19
+
20
+ // layout
21
+ $grid-padding: 15px; // half gutter
22
+
23
+ // fonts
24
+ $primary-font-family: var(--font-family);
25
+ $header-font-family: var(--font-family);
26
+ $code-font-family: var(--code-font-family);
27
+
28
+ // primary palette
29
+ $default-color: #ddd;
30
+ $primary-color: #5f9fd6;
31
+ $secondary-color: #ed5f82;
32
+ $highlight-color: #df6589;
33
+ $code-color: #c92a2a;
34
+ $text-1-color: #555;
35
+ $text-2-color: lighten($text-1-color, 15%);
36
+ $text-3-color: lighten($text-1-color, 30%);
37
+ $header-1-color: #3d3d3d;
38
+ $header-2-color: lighten($header-1-color, 20%);
39
+ $blockquote-color: #999;
40
+ $page-bg-color: #edf0f2;
41
+ $content-bg-color: #fff;
42
+ $border-1-color: $default-color;
43
+ $border-2-color: lighten($default-color, 20%);
44
+
45
+ $immersive-primary-color: #ffef87;
46
+ $immersive-text-color: #ffffff;
47
+ $immersive-header-color: #ffffff;
48
+
49
+ $success-color: #25ad40;
50
+ $fail-color: #fb5555;
51
+ $progress-color: #f39800;
52
+ $invalid-color: #9fa0a0;
53
+
54
+ // sizes
55
+ $font-size: 16px; // main content
56
+ $font-size-secondary: 14px; // secondary content
57
+ $font-size-small: 13px; // additional informations, usually gray colored
58
+ $font-size-icon: 16px;
59
+ $font-size-title: 20px;
60
+
61
+ // components
62
+ $section-gap-v: 15px;
63
+ $section-gap-h: $grid-padding;
64
+ $section-list-v: 25px;
65
+ $section-margin: 30px;
66
+ $section-shadow: rem(0 6px 22px) rgba(#AFC2C9, 0.5);
67
+ $section-bg-color: $content-bg-color;
68
+ $section-title-color: $header-2-color;
69
+ $section-toolbtn-color: #AAA;
70
+ $section-toolbtn-color-hover: #444;
71
+
72
+ $autocomplete-empty-row-color: #AAA;
73
+ $userselect-uid-color: #BBB;
74
+
75
+ $editor-border-color: $border-1-color;
76
+
77
+ $datepicker-shadow: rem(0 2px 7px) rgba(#000, 0.3);
78
+ $datapicker-list-item-bg-color-hover: #F0F0F0;
79
+
80
+ $dialog-layer-bg-color: rgba(#F0F0F0, 0.6);
81
+ $dialog-bg-color: $content-bg-color;
82
+ $dialog-border-color: $border-1-color;
83
+ $dialog-shadow: rem(0 2px 10px) rgba(#000, 0.2);
84
+ $dialog-title-color: $header-2-color;
85
+
86
+ $comment-placeholder-color: #AAA;
87
+ $comment-op-color: lighten($text-1-color, 30%);
88
+ $comment-op-link-color: lighten($text-1-color, 50%);
89
+
90
+ $menu-item-bg-color-hover: #F4F4F4;
91
+ $menu-drop-shadow: rem(0 2px 7px) rgba(#000, 0.3);
92
+ $menu-drop-bg-color: $content-bg-color;
93
+ $menu-drop-triangle-size: 4px;
94
+ $menu-drop-triangle-shadow: rem(-2px -2px 2px) rgba(#000, 0.15);
95
+
96
+ $nav-item-height: 45px;
97
+ $nav-item-hover-color: $primary-color;
98
+ $nav-item-active-color: $secondary-color;
99
+ $nav-item-active-immersive-color: #9AE3F3;
100
+ $nav-item-round-height: 32px;
101
+ $nav-item-round-color: $primary-color;
102
+ $nav-item-round-border: 2px;
103
+ // $nav-logo-height: 32px;
104
+ // $nav-logo-height-mobile: 23px;
105
+
106
+ $header-layer-height: 100px;
107
+ $header-bg-width: 1920px;
108
+ $header-bg-height: 320px;
109
+ $header-bg-height-mini: $header-bg-height - $header-layer-height;
110
+ //$header-logo-width = 280px
111
+ //$header-logo-height = 150px
112
+ //$header-logo-width-real = 280px
113
+ //$header-logo-height-real = 150px
114
+ $header-logo-width: 252px;
115
+ $header-logo-height: 150px;
116
+ $header-logo-width-real: 381px;
117
+ $header-logo-height-real: 201px;
118
+ $header-logo-domain-lh: 60px;
119
+ $header-logo-system-lh: 30px;
120
+
121
+ $footer-bg-color: $content-bg-color;
122
+
123
+ $input-focus-color: #C1E0FF;
124
+ $form-control-height: 35px;
125
+ $compact-control-height: 28px;
126
+ $input-margin: 0 0 1rem;
127
+ $input-multi-line-margin: 0 0.3rem 0.15rem 0;
128
+ $input-image-radio-margin: 0 0.3rem 0.3rem 0;
129
+ $input-background-color: $content-bg-color;
130
+ $input-border: 1px solid $border-1-color;
131
+ $input-outline: 2px solid transparent;
132
+ $input-focus-border-color: $primary-color;
133
+ $input-focus-outline: 2px solid $input-focus-color;
134
+ $label-color: lighten($text-1-color, 10%);
135
+ $help-text-color: lighten($text-1-color, 30%);
136
+
137
+ $table-border-color: $border-2-color;
138
+ $table-alternate-bg-color: #F4F4F4;
139
+ $table-header-bg-color: rgba(#FFF, 0.95);
140
+ $table-header-shadow: rem(0 3px 2px) rgba(#000, 0.03);
141
+ $table-header-shadow-floating: rem(0 2px 5px) rgba(#000, 0.2);
142
+ $table-padding-h: 10px;
143
+ $table-lh: 26px;
144
+
145
+ $toolbar-bg: #f2f6f7;
146
+ $toolbar-fore: #8395a1;
147
+ $toolbar-fore-hover: #5a6b75;
148
+ $toolbar-bg-hover: #e3e7e8;
149
+ $toolbar-fore-selected: #FFF;
150
+ $toolbar-bg-selected: $secondary-color;
151
+ $toolbar-disabled-opacity: 0.3;
152
+
153
+ $supplementary-color: lighten($text-1-color, 50%);
154
+ $supplementary-border-color: lighten($supplementary-color, 62%);
155
+ $supplementary-link-color: lighten($text-1-color, 40%);
156
+
157
+ $pager-border-color: $table-border-color;
158
+
159
+ $nprogress-color: $primary-color;
160
+
161
+ $syntax-hl-bg-color: #F8F8F8;
162
+
163
+ // // Notification
164
+ // $notification-color = {
165
+ // 'success': #9BDB7B,
166
+ // 'info': #78D6F4,
167
+ // 'warn': #FCD279,
168
+ // 'error': #FD848D,
169
+ // }
170
+
171
+ // $notification-icon = {
172
+ // 'success': $icon-check--circle,
173
+ // 'info': $icon-info--circle,
174
+ // 'warn': $icon-info--circle,
175
+ // 'error': $icon-close--circle,
176
+ // }
177
+
178
+
179
+ @mixin form-styles() {
180
+ appearance: none;
181
+ display: block;
182
+ width: 100%;
183
+ font-size: rem($font-size-secondary);
184
+ margin: $input-margin;
185
+ height: rem($form-control-height);
186
+ line-height: 1.2;
187
+ padding: rem(5px);
188
+ border: $input-border;
189
+
190
+ &.inline {
191
+ display: inline-block;
192
+ width: auto;
193
+ }
194
+ }
195
+
196
+ .fullscreen-content {
197
+ position: absolute;
198
+ left: 0;
199
+ top: $nav-item-height;
200
+ width: 100%;
201
+ bottom: 0;
202
+ }
@@ -0,0 +1,6 @@
1
+ export { default as AutoComplete, type AutoCompleteHandle, type AutoCompleteProps } from './autocomplete/AutoComplete';
2
+ export { default as CustomSelectAutoComplete } from './autocomplete/CustomSelectAutoComplete';
3
+ export { default as ConfigEditor } from './ConfigEditor';
4
+ export { default as Icon } from './Icon';
5
+ export { default as Notification } from './Notification';
6
+ export { default as ComponentsProvider } from './provider';
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { Slide, ToastContainer } from 'react-toastify';
3
+
4
+ interface C {
5
+ i18n: (key: string, ...args: any[]) => string;
6
+ theme: 'light' | 'dark';
7
+ codeFontFamily: string;
8
+ }
9
+
10
+ // eslint-disable-next-line
11
+ export const ComponentsContext = React.createContext<C>({
12
+ i18n: (key) => key,
13
+ theme: 'light',
14
+ codeFontFamily: 'monospace',
15
+ });
16
+
17
+ export default function ComponentsProvider(props: { children: React.ReactNode } & C) {
18
+ return <ComponentsContext.Provider value={props}>
19
+ <ToastContainer
20
+ position="bottom-left"
21
+ autoClose={5000}
22
+ hideProgressBar
23
+ newestOnTop={false}
24
+ closeOnClick={false}
25
+ rtl={false}
26
+ pauseOnFocusLoss
27
+ draggable
28
+ pauseOnHover
29
+ theme="colored"
30
+ transition={Slide}
31
+ />
32
+ {props.children}
33
+ </ComponentsContext.Provider>;
34
+ }
@@ -0,0 +1,21 @@
1
+ declare module '*.svg?react' {
2
+ import { ComponentType } from 'react';
3
+ const content: ComponentType;
4
+ export default content;
5
+ }
6
+ declare module '*.vue' {
7
+ const content: any;
8
+ export default content;
9
+ }
10
+ declare module '*.css' {
11
+ const content: string;
12
+ export default content;
13
+ }
14
+ declare module '*.styl' {
15
+ const content: string;
16
+ export default content;
17
+ }
18
+ declare module '*.scss' {
19
+ const content: string;
20
+ export default content;
21
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@halooj/components",
3
+ "version": "1.0.1",
4
+ "main": "./frontend/index.ts",
5
+ "repository": "https://github.com/hydro-dev/Hydro",
6
+ "sideEffects": [
7
+ "*.scss",
8
+ "*.css"
9
+ ],
10
+ "dependencies": {
11
+ "allotment": "^1.20.5",
12
+ "diff": "^9.0.0",
13
+ "js-yaml": "^4.3.0",
14
+ "lodash": "^4.18.1",
15
+ "react-dnd": "^16.0.1",
16
+ "react-dnd-html5-backend": "^16.0.1",
17
+ "react-toastify": "^11.1.0",
18
+ "schemastery-react": "^0.1.5"
19
+ },
20
+ "devDependencies": {
21
+ "@types/diff": "^8.0.0",
22
+ "@types/js-yaml": "^4.0.9",
23
+ "@types/lodash": "^4.17.24",
24
+ "sass": "^1.101.0"
25
+ },
26
+ "peerDependencies": {
27
+ "react": "*",
28
+ "react-dom": "*",
29
+ "schemastery": "*"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ }
34
+ }