@bccampus/ui-components 0.3.0 → 0.4.0
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.
- package/dist/composite.d.ts +151 -0
- package/dist/composite.js +472 -0
- package/dist/generate-tiles-DuagGD1d.js +244 -0
- package/dist/icon-generator.js +43 -270
- package/dist/igenerate-tiles.d.ts +43 -0
- package/dist/igenerate-tiles.js +7 -0
- package/package.json +12 -1
- package/src/components/ui/composite/CompositeData.ts +215 -0
- package/src/components/ui/composite/CompositeDataItem.ts +144 -0
- package/src/components/ui/composite/composite-component-item.tsx +50 -0
- package/src/components/ui/composite/composite-component.tsx +100 -0
- package/src/components/ui/composite/composite-data-context.tsx +31 -0
- package/src/components/ui/composite/index.ts +4 -0
- package/src/components/ui/composite/types.ts +81 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/hooks/use-effect-after-mount.ts +27 -0
- package/src/hooks/use-id.ts +5 -0
- package/src/hooks/use-keyboard-event.ts +144 -0
- package/src/lib/object.ts +48 -0
- package/src/lib/set-operations.ts +52 -0
- package/tsconfig.node.json +25 -25
- package/vite.config.ts +2 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { KeyboardEvent, KeyboardEventHandler } from 'react';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
const MODIFIER_KEYS = new Set(['ctrl', 'shift', 'alt', 'meta']);
|
|
5
|
+
|
|
6
|
+
const KEY_MAPPINGS: Record<string, string> = {
|
|
7
|
+
' ': 'space',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export interface KeyBindings {
|
|
11
|
+
[sequence: string]: (event: KeyboardEvent) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface KeybindingLookupItem {
|
|
15
|
+
sequence: Set<string>;
|
|
16
|
+
handler: (event: KeyboardEvent) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface UseKeyboardEventOptions {
|
|
20
|
+
eventKeyProp: 'key' | 'code';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isSequenceEqual<T>(sequenceA: Set<T>, sequenceB: Set<T>) {
|
|
24
|
+
if (sequenceA.size !== sequenceB.size) return false;
|
|
25
|
+
|
|
26
|
+
for (const element of sequenceB) {
|
|
27
|
+
if (!sequenceA.has(element)) return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseKeybindings(bindings: KeyBindings) {
|
|
34
|
+
const parsedKeybindings: KeybindingLookupItem[] = [];
|
|
35
|
+
for (const [sequence, handler] of Object.entries(bindings)) {
|
|
36
|
+
const parsedSequence = sequence
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.trim()
|
|
39
|
+
.split(/\s*\+\s*/);
|
|
40
|
+
|
|
41
|
+
if (parsedSequence.length === 1 && MODIFIER_KEYS.has(parsedSequence[0])) {
|
|
42
|
+
console.error(`[useKeyboardEvent] \`${sequence}\`: A key sequence cannot be only a modifier key.`);
|
|
43
|
+
}
|
|
44
|
+
else if (parsedSequence.includes('')) {
|
|
45
|
+
console.error(`[useKeyboardEvent] \`${sequence}\`: Unknown key defined in the sequence.`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
parsedKeybindings.push({
|
|
49
|
+
sequence: new Set(parsedSequence),
|
|
50
|
+
handler,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return parsedKeybindings;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const defaultOptions: UseKeyboardEventOptions = {
|
|
59
|
+
eventKeyProp: 'key',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export function keyboardEventHandler(
|
|
63
|
+
bindings: KeyBindings,
|
|
64
|
+
options: UseKeyboardEventOptions = defaultOptions
|
|
65
|
+
): KeyboardEventHandler {
|
|
66
|
+
const _options = { ...options, ...defaultOptions }
|
|
67
|
+
|
|
68
|
+
const keyBindings = parseKeybindings(bindings);
|
|
69
|
+
|
|
70
|
+
return (event: KeyboardEvent) => {
|
|
71
|
+
const keySequence = new Set<string>();
|
|
72
|
+
const eventKey = event[_options.eventKeyProp];
|
|
73
|
+
|
|
74
|
+
if (event.ctrlKey) keySequence.add('ctrl');
|
|
75
|
+
|
|
76
|
+
if (event.shiftKey) keySequence.add('shift');
|
|
77
|
+
|
|
78
|
+
if (event.altKey) keySequence.add('alt');
|
|
79
|
+
|
|
80
|
+
if (event.metaKey) keySequence.add('meta');
|
|
81
|
+
|
|
82
|
+
if (!KEY_MAPPINGS[eventKey]) keySequence.add(eventKey.toLowerCase());
|
|
83
|
+
else keySequence.add(KEY_MAPPINGS[eventKey]);
|
|
84
|
+
|
|
85
|
+
const matchedSequence = keyBindings.find(keyBinding => isSequenceEqual(keySequence, keyBinding.sequence));
|
|
86
|
+
if (matchedSequence) {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
event.stopPropagation();
|
|
89
|
+
matchedSequence.handler(event);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns a `KeyboardEventHandler`
|
|
96
|
+
* that checks the defined key binding sequences against a keyboard event
|
|
97
|
+
* and executes the handler of the first matched key binding.
|
|
98
|
+
*
|
|
99
|
+
* Limitations:
|
|
100
|
+
* - Space character (` `) cannot be used in the key sequences.
|
|
101
|
+
* Use the `space` keyword instead.
|
|
102
|
+
* - Plus character (`+`) cannot be used in the key sequences.
|
|
103
|
+
* Use `shit + =` instead.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```
|
|
107
|
+
* export function Input({ value, onChange }: Props) {
|
|
108
|
+
* const [inputValue, setInputValue] = useState<string>('');
|
|
109
|
+
*
|
|
110
|
+
* const clearInput = () => {
|
|
111
|
+
* setInputValue('');
|
|
112
|
+
* };
|
|
113
|
+
*
|
|
114
|
+
* const addItem = () => {
|
|
115
|
+
* if (inputValue) {
|
|
116
|
+
* onChange([...value, inputValue]);
|
|
117
|
+
* clearInput();
|
|
118
|
+
* }
|
|
119
|
+
* };
|
|
120
|
+
*
|
|
121
|
+
* const deleteAll = () => {
|
|
122
|
+
* onChange([]);
|
|
123
|
+
* clearInput();
|
|
124
|
+
* };
|
|
125
|
+
*
|
|
126
|
+
* const handleKeyDown = useKeyboardEvent({
|
|
127
|
+
* 'enter': addItem,
|
|
128
|
+
* 'escape': clearInput,
|
|
129
|
+
* 'ctrl+c': clearInput,
|
|
130
|
+
* 'ctrl + shift + c': deleteAll,
|
|
131
|
+
* });
|
|
132
|
+
*
|
|
133
|
+
* return (
|
|
134
|
+
* <input
|
|
135
|
+
* value={inputValue}
|
|
136
|
+
* onChange={event => setInputValue(event.target.value)}
|
|
137
|
+
* onKeyDown={handleKeyDown}
|
|
138
|
+
* />;
|
|
139
|
+
* }
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export function useKeyboardEvent(bindings: KeyBindings, options?: UseKeyboardEventOptions) {
|
|
143
|
+
return useMemo(() => keyboardEventHandler(bindings, options), [bindings, options]);
|
|
144
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
export const get = <T extends object>(object: T, prop: string) => prop
|
|
4
|
+
.split('.')
|
|
5
|
+
.reduce<any>((reducedObject, key) => (reducedObject && key in reducedObject) ? reducedObject[key] : undefined, object);
|
|
6
|
+
|
|
7
|
+
export const set = <T extends object, V>(object: T, prop: string, value: V) => {
|
|
8
|
+
const propChunks = prop.split('.');
|
|
9
|
+
const lastChunk = propChunks.pop();
|
|
10
|
+
if (!lastChunk) return object;
|
|
11
|
+
|
|
12
|
+
const ref = propChunks.reduce<any>((reducedObject, key) => {
|
|
13
|
+
reducedObject[key] = {};
|
|
14
|
+
return reducedObject[key];
|
|
15
|
+
}, object);
|
|
16
|
+
|
|
17
|
+
ref[lastChunk] = value;
|
|
18
|
+
|
|
19
|
+
return object;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const pick = <T extends object>(object: T, props: string[]) => {
|
|
23
|
+
|
|
24
|
+
return props.reduce<Record<string, unknown>>((result, key) => {
|
|
25
|
+
|
|
26
|
+
set(result, key, get(object, key));
|
|
27
|
+
|
|
28
|
+
return result;
|
|
29
|
+
}, {}) as Partial<T>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const omit = <T extends object>(object: T, props: string[]) => {
|
|
33
|
+
const result: Partial<T> = { ...object };
|
|
34
|
+
|
|
35
|
+
props.forEach(prop => {
|
|
36
|
+
const propChunks = prop.split('.');
|
|
37
|
+
const lastChunk = propChunks.pop();
|
|
38
|
+
if (lastChunk) {
|
|
39
|
+
const ref = propChunks.reduce<any>((reducedObject, key) => (reducedObject && key in reducedObject) ? reducedObject[key] : undefined, result);
|
|
40
|
+
if (ref && lastChunk in ref) delete ref[lastChunk];
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const isObject = (object: unknown) => (typeof object === 'object' && !Array.isArray(object) && object !== null);
|
|
48
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
export function isSuperset(set: Iterable<any>, subset: Iterable<any>) {
|
|
4
|
+
const _set = set instanceof Set ? set : new Set(set);
|
|
5
|
+
|
|
6
|
+
for (const elem of subset) {
|
|
7
|
+
if (!_set.has(elem)) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function union(setA: Iterable<any>, setB: Iterable<any>) {
|
|
15
|
+
const _union = new Set(setA);
|
|
16
|
+
for (const elem of setB) {
|
|
17
|
+
_union.add(elem);
|
|
18
|
+
}
|
|
19
|
+
return _union;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function intersection(setA: Iterable<any>, setB: Iterable<any>) {
|
|
23
|
+
const _setA = setA instanceof Set ? setA : new Set(setA);
|
|
24
|
+
|
|
25
|
+
const _intersection = new Set();
|
|
26
|
+
for (const elem of setB) {
|
|
27
|
+
if (_setA.has(elem)) {
|
|
28
|
+
_intersection.add(elem);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return _intersection;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function symmetricDifference(setA: Iterable<any>, setB: Iterable<any>) {
|
|
35
|
+
const _difference = new Set(setA);
|
|
36
|
+
for (const elem of setB) {
|
|
37
|
+
if (_difference.has(elem)) {
|
|
38
|
+
_difference.delete(elem);
|
|
39
|
+
} else {
|
|
40
|
+
_difference.add(elem);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return _difference;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function difference(setA: Iterable<any>, setB: Iterable<any>) {
|
|
47
|
+
const _difference = new Set(setA);
|
|
48
|
+
for (const elem of setB) {
|
|
49
|
+
_difference.delete(elem);
|
|
50
|
+
}
|
|
51
|
+
return _difference;
|
|
52
|
+
}
|
package/tsconfig.node.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
-
"target": "ES2023",
|
|
5
|
-
"lib": ["ES2023"],
|
|
6
|
-
"module": "ESNext",
|
|
7
|
-
"skipLibCheck": true,
|
|
8
|
-
|
|
9
|
-
/* Bundler mode */
|
|
10
|
-
"moduleResolution": "bundler",
|
|
11
|
-
"allowImportingTsExtensions": true,
|
|
12
|
-
"verbatimModuleSyntax": true,
|
|
13
|
-
"moduleDetection": "force",
|
|
14
|
-
"noEmit": true,
|
|
15
|
-
|
|
16
|
-
/* Linting */
|
|
17
|
-
"strict": true,
|
|
18
|
-
"noUnusedLocals": true,
|
|
19
|
-
"noUnusedParameters": true,
|
|
20
|
-
"erasableSyntaxOnly": true,
|
|
21
|
-
"noFallthroughCasesInSwitch": true,
|
|
22
|
-
"noUncheckedSideEffectImports": true
|
|
23
|
-
},
|
|
24
|
-
"include": ["vite.config.ts"]
|
|
25
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
|
|
16
|
+
/* Linting */
|
|
17
|
+
"strict": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"erasableSyntaxOnly": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"noUncheckedSideEffectImports": true
|
|
23
|
+
},
|
|
24
|
+
"include": ["vite.config.ts"]
|
|
25
|
+
}
|
package/vite.config.ts
CHANGED
|
@@ -20,7 +20,9 @@ export default defineConfig({
|
|
|
20
20
|
formats: ['es'],
|
|
21
21
|
entry: {
|
|
22
22
|
'ui-components': path.resolve(__dirname, 'src/components/ui/index.ts'),
|
|
23
|
+
'composite': path.resolve(__dirname, 'src/components/ui/composite/index.ts'),
|
|
23
24
|
'icon-generator': path.resolve(__dirname, 'src/components/ui/icon-generator/icon-generator.tsx'),
|
|
25
|
+
'igenerate-tiles': path.resolve(__dirname, 'src/components/ui/icon-generator/generate-tiles.tsx'),
|
|
24
26
|
'masked-image-generator': path.resolve(__dirname, 'src/components/ui/icon-generator/masked-image-generator.tsx'),
|
|
25
27
|
'caption': path.resolve(__dirname, 'src/components/ui/typography/caption.tsx'),
|
|
26
28
|
'banner': path.resolve(__dirname, 'src/components/ui/banner.tsx'),
|