@bgord/ui 0.7.9 → 0.8.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.
- package/dist/components/dialog.d.ts +2 -3
- package/dist/components/dialog.js +20 -0
- package/dist/components/index.js +1 -0
- package/dist/hooks/index.js +13 -0
- package/dist/hooks/use-click-outside.js +31 -0
- package/dist/hooks/use-date-field.js +36 -0
- package/dist/hooks/use-exit-action.d.ts +0 -1
- package/dist/hooks/use-exit-action.js +23 -0
- package/dist/hooks/use-file.js +52 -0
- package/dist/hooks/use-focus-shortcut.js +12 -0
- package/dist/hooks/use-hover.js +22 -0
- package/dist/hooks/use-meta-enter-submit.js +10 -0
- package/dist/hooks/use-mutation.js +57 -0
- package/dist/hooks/use-number-field.js +39 -0
- package/dist/hooks/use-scroll-lock.js +13 -0
- package/dist/hooks/use-shortcuts.js +11 -0
- package/dist/hooks/use-text-field.js +30 -0
- package/dist/hooks/use-toggle.js +22 -0
- package/dist/index.js +3 -1
- package/dist/services/autocomplete.js +5 -0
- package/dist/services/clipboard.js +10 -0
- package/dist/services/cookies.js +9 -0
- package/dist/services/date-field.js +21 -0
- package/dist/services/etag.js +5 -0
- package/dist/services/exec.js +6 -0
- package/dist/services/fields.js +17 -0
- package/dist/services/form.d.ts +0 -1
- package/dist/services/form.js +54 -0
- package/dist/services/get-safe-window.js +5 -0
- package/dist/services/head.js +9 -0
- package/dist/services/index.js +18 -0
- package/dist/services/noop.js +1 -0
- package/dist/services/number-field.js +23 -0
- package/dist/services/pluralize.js +23 -0
- package/dist/services/rhythm.js +30 -0
- package/dist/services/text-field.js +23 -0
- package/dist/services/time-zone-offset.js +5 -0
- package/dist/services/translations.js +44 -0
- package/dist/services/weak-etag.js +5 -0
- package/package.json +1 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
export type DialogPropsType = hooks.UseToggleReturnType & React.JSX.IntrinsicElements["dialog"] & {
|
|
1
|
+
import { type UseToggleReturnType } from "../hooks";
|
|
2
|
+
export type DialogPropsType = UseToggleReturnType & React.JSX.IntrinsicElements["dialog"] & {
|
|
4
3
|
locked?: boolean;
|
|
5
4
|
};
|
|
6
5
|
export declare function Dialog(props: DialogPropsType): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { extractUseToggle, useClickOutside, useScrollLock, useShortcuts, } from "../hooks";
|
|
4
|
+
import { noop } from "../services/noop";
|
|
5
|
+
export function Dialog(props) {
|
|
6
|
+
const { locked: _locked, ..._props } = props;
|
|
7
|
+
const locked = _locked ?? false;
|
|
8
|
+
const { toggle: dialog, rest } = extractUseToggle(_props);
|
|
9
|
+
const ref = useRef(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (props.on)
|
|
12
|
+
ref.current?.showModal();
|
|
13
|
+
else
|
|
14
|
+
ref.current?.close();
|
|
15
|
+
}, [props.on]);
|
|
16
|
+
useShortcuts({ Escape: locked ? noop : dialog.disable });
|
|
17
|
+
useScrollLock(props.on);
|
|
18
|
+
useClickOutside(ref, locked ? noop : dialog.disable);
|
|
19
|
+
return (_jsx("dialog", { ref: ref, tabIndex: 0, "aria-modal": "true", "data-disp": props.on ? "flex" : "none", "data-dir": "column", "data-mx": "auto", "data-p": "5", "data-position": "fixed", "data-z": "2", "data-bg": "neutral-900", "data-br": "xs", "data-backdrop": "stronger", "data-animation": "grow-fade-in", ...rest }));
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./dialog";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./use-click-outside";
|
|
2
|
+
export * from "./use-date-field";
|
|
3
|
+
export * from "./use-exit-action";
|
|
4
|
+
export * from "./use-file";
|
|
5
|
+
export * from "./use-focus-shortcut";
|
|
6
|
+
export * from "./use-hover";
|
|
7
|
+
export * from "./use-meta-enter-submit";
|
|
8
|
+
export * from "./use-mutation";
|
|
9
|
+
export * from "./use-number-field";
|
|
10
|
+
export * from "./use-scroll-lock";
|
|
11
|
+
export * from "./use-shortcuts";
|
|
12
|
+
export * from "./use-text-field";
|
|
13
|
+
export * from "./use-toggle";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
export function useClickOutside(ref, handler) {
|
|
3
|
+
useEffect(() => {
|
|
4
|
+
if (typeof document === "undefined")
|
|
5
|
+
return; // SSR-guard
|
|
6
|
+
function listener(event) {
|
|
7
|
+
const element = ref.current;
|
|
8
|
+
if (!element)
|
|
9
|
+
return;
|
|
10
|
+
if (element.contains(event.target)) {
|
|
11
|
+
if (event.target === element) {
|
|
12
|
+
const { left, right, top, bottom } = element.getBoundingClientRect();
|
|
13
|
+
const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX;
|
|
14
|
+
const clientY = event instanceof MouseEvent ? event.clientY : event.touches[0].clientY;
|
|
15
|
+
const targetsElement = clientX >= left && clientX <= right && clientY >= top && clientY <= bottom;
|
|
16
|
+
if (targetsElement)
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
else
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
handler(event);
|
|
23
|
+
}
|
|
24
|
+
document.addEventListener("mousedown", listener);
|
|
25
|
+
document.addEventListener("touchstart", listener);
|
|
26
|
+
return () => {
|
|
27
|
+
document.removeEventListener("mousedown", listener);
|
|
28
|
+
document.removeEventListener("touchstart", listener);
|
|
29
|
+
};
|
|
30
|
+
}, [ref, handler]);
|
|
31
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { DateField } from "../services/date-field";
|
|
3
|
+
export function useDateField(config) {
|
|
4
|
+
const defaultValue = new DateField(config.defaultValue);
|
|
5
|
+
const [value, setValue] = useState(defaultValue.get());
|
|
6
|
+
const [dom, setDom] = useState(DateField.isEmpty(defaultValue.get()) ? "" : defaultValue.get());
|
|
7
|
+
const setCurrentValue = (next) => {
|
|
8
|
+
const date = new DateField(next).get();
|
|
9
|
+
setValue(date);
|
|
10
|
+
setDom(DateField.isEmpty(date) ? "" : String(date));
|
|
11
|
+
};
|
|
12
|
+
const onChange = (event) => {
|
|
13
|
+
const element = event.currentTarget;
|
|
14
|
+
const value = element.value;
|
|
15
|
+
setDom(value);
|
|
16
|
+
if (value === "")
|
|
17
|
+
return setValue(DateField.EMPTY);
|
|
18
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
19
|
+
return;
|
|
20
|
+
if (!element.validity.valid)
|
|
21
|
+
return;
|
|
22
|
+
setValue(value);
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
defaultValue: defaultValue.get(),
|
|
26
|
+
value,
|
|
27
|
+
set: setCurrentValue,
|
|
28
|
+
handleChange: onChange,
|
|
29
|
+
clear: () => setCurrentValue(defaultValue.get()),
|
|
30
|
+
label: { props: { htmlFor: config.name } },
|
|
31
|
+
input: { props: { id: config.name, name: config.name, value: dom, onChange } },
|
|
32
|
+
changed: !DateField.compare(value, defaultValue.get()),
|
|
33
|
+
unchanged: DateField.compare(value, defaultValue.get()),
|
|
34
|
+
empty: DateField.isEmpty(value),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
var UseExitActionPhase;
|
|
3
|
+
(function (UseExitActionPhase) {
|
|
4
|
+
UseExitActionPhase["idle"] = "idle";
|
|
5
|
+
UseExitActionPhase["exiting"] = "exiting";
|
|
6
|
+
UseExitActionPhase["gone"] = "gone";
|
|
7
|
+
})(UseExitActionPhase || (UseExitActionPhase = {}));
|
|
8
|
+
export function useExitAction(options) {
|
|
9
|
+
const [phase, setPhase] = useState(UseExitActionPhase.idle);
|
|
10
|
+
const trigger = (event) => {
|
|
11
|
+
event.preventDefault();
|
|
12
|
+
if (phase === "idle")
|
|
13
|
+
setPhase(UseExitActionPhase.exiting);
|
|
14
|
+
};
|
|
15
|
+
const onAnimationEnd = async (event) => {
|
|
16
|
+
if (event.animationName !== options.animation)
|
|
17
|
+
return;
|
|
18
|
+
setPhase(UseExitActionPhase.gone);
|
|
19
|
+
await options.action();
|
|
20
|
+
};
|
|
21
|
+
const attach = phase === "exiting" ? { "data-animation": options.animation, onAnimationEnd } : undefined;
|
|
22
|
+
return { visible: phase !== "gone", attach, trigger };
|
|
23
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
export var UseFileState;
|
|
3
|
+
(function (UseFileState) {
|
|
4
|
+
UseFileState["idle"] = "idle";
|
|
5
|
+
UseFileState["selected"] = "selected";
|
|
6
|
+
UseFileState["error"] = "error";
|
|
7
|
+
})(UseFileState || (UseFileState = {}));
|
|
8
|
+
export function useFile(name, config) {
|
|
9
|
+
const maxSizeBytes = config?.maxSizeBytes ?? Number.POSITIVE_INFINITY;
|
|
10
|
+
const [key, setKey] = useState(0);
|
|
11
|
+
const [state, setState] = useState(UseFileState.idle);
|
|
12
|
+
const [file, setFile] = useState(null);
|
|
13
|
+
function selectFile(event) {
|
|
14
|
+
const files = event.currentTarget.files;
|
|
15
|
+
if (!files?.[0])
|
|
16
|
+
return;
|
|
17
|
+
const file = files[0];
|
|
18
|
+
if (file.size > maxSizeBytes) {
|
|
19
|
+
setState(UseFileState.error);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!config.mimeTypes.includes(file.type)) {
|
|
23
|
+
setState(UseFileState.error);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
setFile(file);
|
|
27
|
+
setState(UseFileState.selected);
|
|
28
|
+
return file;
|
|
29
|
+
}
|
|
30
|
+
function clearFile() {
|
|
31
|
+
setKey((key) => key + 1);
|
|
32
|
+
setFile(null);
|
|
33
|
+
setState(UseFileState.idle);
|
|
34
|
+
}
|
|
35
|
+
const preview = useMemo(() => (file ? URL.createObjectURL(file) : undefined), [file]);
|
|
36
|
+
function matches(states) {
|
|
37
|
+
return states.some((given) => given === state);
|
|
38
|
+
}
|
|
39
|
+
const props = {
|
|
40
|
+
actions: { selectFile, clearFile },
|
|
41
|
+
input: { props: { id: name, name, multiple: false, accept: config.mimeTypes.join(",") }, key },
|
|
42
|
+
label: { props: { htmlFor: name } },
|
|
43
|
+
matches,
|
|
44
|
+
};
|
|
45
|
+
if (state === UseFileState.idle) {
|
|
46
|
+
return { data: null, isError: false, isIdle: true, isSelected: false, state, ...props };
|
|
47
|
+
}
|
|
48
|
+
if (state === UseFileState.selected) {
|
|
49
|
+
return { data: file, isError: false, isIdle: false, isSelected: true, preview, state, ...props };
|
|
50
|
+
}
|
|
51
|
+
return { data: null, isError: true, isIdle: false, isSelected: false, state, ...props };
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
2
|
+
import { useShortcuts } from "./use-shortcuts";
|
|
3
|
+
export function useFocusKeyboardShortcut(shortcut) {
|
|
4
|
+
const ref = useRef(null);
|
|
5
|
+
const handleFocus = useCallback(() => {
|
|
6
|
+
if (ref.current) {
|
|
7
|
+
ref.current.focus();
|
|
8
|
+
}
|
|
9
|
+
}, []);
|
|
10
|
+
useShortcuts({ [shortcut]: handleFocus });
|
|
11
|
+
return useMemo(() => ({ ref }), []);
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
import { useToggle } from "./use-toggle";
|
|
3
|
+
export function useHover(config) {
|
|
4
|
+
const enabled = config?.enabled ?? true;
|
|
5
|
+
const toggle = useToggle({ name: "_internal" });
|
|
6
|
+
const element = useRef(null);
|
|
7
|
+
const enterEvent = typeof window !== "undefined" && "PointerEvent" in window ? "pointerenter" : "mouseenter";
|
|
8
|
+
const leaveEvent = typeof window !== "undefined" && "PointerEvent" in window ? "pointerleave" : "mouseleave";
|
|
9
|
+
const ref = useCallback((node) => {
|
|
10
|
+
const previous = element.current;
|
|
11
|
+
if (previous) {
|
|
12
|
+
previous.removeEventListener(enterEvent, toggle.enable);
|
|
13
|
+
previous.removeEventListener(leaveEvent, toggle.disable);
|
|
14
|
+
}
|
|
15
|
+
element.current = node;
|
|
16
|
+
if (node && enabled) {
|
|
17
|
+
node.addEventListener(enterEvent, toggle.enable);
|
|
18
|
+
node.addEventListener(leaveEvent, toggle.disable);
|
|
19
|
+
}
|
|
20
|
+
}, [enterEvent, leaveEvent, enabled, toggle.enable, toggle.disable]);
|
|
21
|
+
return { attach: { ref }, hovering: toggle.on && enabled };
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
export function useMetaEnterSubmit() {
|
|
3
|
+
const onKeyDown = useCallback((event) => {
|
|
4
|
+
if (!event.metaKey || event.key !== "Enter")
|
|
5
|
+
return;
|
|
6
|
+
event.preventDefault();
|
|
7
|
+
event.currentTarget.form?.requestSubmit();
|
|
8
|
+
}, []);
|
|
9
|
+
return { onKeyDown };
|
|
10
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
export var MutationState;
|
|
3
|
+
(function (MutationState) {
|
|
4
|
+
MutationState["idle"] = "idle";
|
|
5
|
+
MutationState["loading"] = "loading";
|
|
6
|
+
MutationState["error"] = "error";
|
|
7
|
+
MutationState["done"] = "done";
|
|
8
|
+
})(MutationState || (MutationState = {}));
|
|
9
|
+
export function useMutation(options) {
|
|
10
|
+
const [state, setState] = useState(MutationState.idle);
|
|
11
|
+
const [error, setError] = useState(null);
|
|
12
|
+
const reset = useCallback(() => {
|
|
13
|
+
setError(null);
|
|
14
|
+
setState(MutationState.idle);
|
|
15
|
+
}, []);
|
|
16
|
+
const mutate = useCallback(async () => {
|
|
17
|
+
if (state === MutationState.loading)
|
|
18
|
+
return;
|
|
19
|
+
setError(null);
|
|
20
|
+
setState(MutationState.loading);
|
|
21
|
+
try {
|
|
22
|
+
const response = await options.perform();
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
setState(MutationState.error);
|
|
25
|
+
setError(null);
|
|
26
|
+
await options.onError?.(null);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
setState(MutationState.done);
|
|
30
|
+
await options.onSuccess?.(response);
|
|
31
|
+
if (options.autoResetDelayMs) {
|
|
32
|
+
setTimeout(() => setState(MutationState.idle), options.autoResetDelayMs);
|
|
33
|
+
}
|
|
34
|
+
return response;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
setState(MutationState.error);
|
|
38
|
+
setError(error);
|
|
39
|
+
await options.onError?.(error);
|
|
40
|
+
}
|
|
41
|
+
}, [state, options]);
|
|
42
|
+
const handleSubmit = useCallback(async (event) => {
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
await mutate();
|
|
45
|
+
}, [mutate]);
|
|
46
|
+
return {
|
|
47
|
+
state,
|
|
48
|
+
error,
|
|
49
|
+
isIdle: state === MutationState.idle,
|
|
50
|
+
isLoading: state === MutationState.loading,
|
|
51
|
+
isError: state === MutationState.error,
|
|
52
|
+
isDone: state === MutationState.done,
|
|
53
|
+
mutate,
|
|
54
|
+
handleSubmit,
|
|
55
|
+
reset,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { NumberField } from "../services/number-field";
|
|
3
|
+
export function useNumberField(config) {
|
|
4
|
+
const defaultValue = new NumberField(config.defaultValue);
|
|
5
|
+
const [value, setValue] = useState(defaultValue.get());
|
|
6
|
+
const [dom, setDom] = useState(NumberField.isEmpty(defaultValue.get()) ? "" : String(defaultValue.get()));
|
|
7
|
+
const setCurrentValue = (next) => {
|
|
8
|
+
const number = new NumberField(next).get();
|
|
9
|
+
setValue(number);
|
|
10
|
+
setDom(NumberField.isEmpty(number) ? "" : String(number));
|
|
11
|
+
};
|
|
12
|
+
const onChange = (event) => {
|
|
13
|
+
const element = event.currentTarget;
|
|
14
|
+
const dom = element.value;
|
|
15
|
+
setDom(dom);
|
|
16
|
+
if (dom === "")
|
|
17
|
+
return setValue(NumberField.EMPTY);
|
|
18
|
+
// Validating before setting the state value
|
|
19
|
+
// to avoid undefined when typing 1. when trying to input 1.5
|
|
20
|
+
const value = element.valueAsNumber;
|
|
21
|
+
if (!Number.isFinite(value))
|
|
22
|
+
return;
|
|
23
|
+
if (!event.currentTarget.validity.valid)
|
|
24
|
+
return;
|
|
25
|
+
setValue(value);
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
defaultValue: defaultValue.get(),
|
|
29
|
+
value,
|
|
30
|
+
set: setCurrentValue,
|
|
31
|
+
handleChange: onChange,
|
|
32
|
+
clear: () => setCurrentValue(defaultValue.get()),
|
|
33
|
+
label: { props: { htmlFor: config.name } },
|
|
34
|
+
input: { props: { id: config.name, name: config.name, value: dom, onChange } },
|
|
35
|
+
changed: !NumberField.compare(value, defaultValue.get()),
|
|
36
|
+
unchanged: NumberField.compare(value, defaultValue.get()),
|
|
37
|
+
empty: NumberField.isEmpty(value),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
export function useScrollLock(enabled = true) {
|
|
3
|
+
useEffect(() => {
|
|
4
|
+
if (typeof document === "undefined")
|
|
5
|
+
return;
|
|
6
|
+
const original = document.body.style.overflow;
|
|
7
|
+
if (enabled)
|
|
8
|
+
document.body.style.overflow = "hidden";
|
|
9
|
+
return () => {
|
|
10
|
+
document.body.style.overflow = original;
|
|
11
|
+
};
|
|
12
|
+
}, [enabled]);
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { tinykeys } from "tinykeys";
|
|
3
|
+
export function useShortcuts(config, options) {
|
|
4
|
+
const enabled = options?.enabled ?? true;
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
if (!enabled)
|
|
7
|
+
return;
|
|
8
|
+
const unsubscribe = tinykeys(window, config);
|
|
9
|
+
return () => unsubscribe();
|
|
10
|
+
}, [config, enabled]);
|
|
11
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { TextField } from "../services/text-field";
|
|
3
|
+
export function useTextField(config) {
|
|
4
|
+
const defaultValue = new TextField(config.defaultValue);
|
|
5
|
+
const [value, setValue] = useState(defaultValue.get());
|
|
6
|
+
const setCurrentValue = (next) => setValue(new TextField(next).get());
|
|
7
|
+
const onChange = (event) => setCurrentValue(event.currentTarget.value);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
setCurrentValue(defaultValue.get());
|
|
10
|
+
}, [config.defaultValue]);
|
|
11
|
+
return {
|
|
12
|
+
defaultValue: defaultValue.get(),
|
|
13
|
+
value,
|
|
14
|
+
set: setCurrentValue,
|
|
15
|
+
handleChange: onChange,
|
|
16
|
+
clear: () => setCurrentValue(defaultValue.get()),
|
|
17
|
+
label: { props: { htmlFor: config.name } },
|
|
18
|
+
input: {
|
|
19
|
+
props: {
|
|
20
|
+
id: config.name,
|
|
21
|
+
name: config.name,
|
|
22
|
+
value: TextField.isEmpty(value) ? "" : value,
|
|
23
|
+
onChange,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
changed: !TextField.compare(value, defaultValue.get()),
|
|
27
|
+
unchanged: TextField.compare(value, defaultValue.get()),
|
|
28
|
+
empty: TextField.isEmpty(value),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
export function useToggle({ name, defaultValue = false }) {
|
|
3
|
+
const [on, setOn] = useState(defaultValue);
|
|
4
|
+
const enable = () => setOn(true);
|
|
5
|
+
const disable = () => setOn(false);
|
|
6
|
+
const toggle = () => setOn((v) => !v);
|
|
7
|
+
const off = !on;
|
|
8
|
+
const props = {
|
|
9
|
+
controller: {
|
|
10
|
+
"aria-expanded": on ? "true" : "false",
|
|
11
|
+
"aria-controls": name,
|
|
12
|
+
role: "button",
|
|
13
|
+
tabIndex: 0,
|
|
14
|
+
},
|
|
15
|
+
target: { id: name, role: "region", "aria-hidden": on ? "false" : "true" },
|
|
16
|
+
};
|
|
17
|
+
return { on, off, enable, disable, toggle, props };
|
|
18
|
+
}
|
|
19
|
+
export function extractUseToggle(_props) {
|
|
20
|
+
const { on, off, enable, disable, toggle, props, ...rest } = _props;
|
|
21
|
+
return { toggle: { on, off, enable, disable, toggle, props }, rest: rest };
|
|
22
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1 +1,3 @@
|
|
|
1
|
-
import{useEffect as Q,useRef as ee}from"react";import{useEffect as H}from"react";function w(e,t){H(()=>{if(typeof document>"u")return;function n(r){let o=e.current;if(!o)return;if(o.contains(r.target))if(r.target===o){let{left:a,right:i,top:l,bottom:s}=o.getBoundingClientRect(),p=r instanceof MouseEvent?r.clientX:r.touches[0].clientX,y=r instanceof MouseEvent?r.clientY:r.touches[0].clientY;if(p>=a&&p<=i&&y>=l&&y<=s)return}else return;t(r)}return document.addEventListener("mousedown",n),document.addEventListener("touchstart",n),()=>{document.removeEventListener("mousedown",n),document.removeEventListener("touchstart",n)}},[e,t])}import{useState as D}from"react";class d{static EMPTY=void 0;value=d.EMPTY;constructor(e){this.value=d.isEmpty(e)?d.EMPTY:e}get(){return this.value}isEmpty(){return d.isEmpty(this.value)}static isEmpty(e){return e===void 0||e===null||e===""}static compare(e,t){if(d.isEmpty(e)&&d.isEmpty(t))return!0;return e===t}}function Ee(e){let t=new d(e.defaultValue),[n,r]=D(t.get()),[o,a]=D(d.isEmpty(t.get())?"":t.get()),i=(s)=>{let p=new d(s).get();r(p),a(d.isEmpty(p)?"":String(p))},l=(s)=>{let p=s.currentTarget,y=p.value;if(a(y),y==="")return r(d.EMPTY);if(!/^\d{4}-\d{2}-\d{2}$/.test(y))return;if(!p.validity.valid)return;r(y)};return{defaultValue:t.get(),value:n,set:i,handleChange:l,clear:()=>i(t.get()),label:{props:{htmlFor:e.name}},input:{props:{id:e.name,name:e.name,value:o,onChange:l}},changed:!d.compare(n,t.get()),unchanged:d.compare(n,t.get()),empty:d.isEmpty(n)}}import k from"react";function Fe(e){let[t,n]=k.useState("idle"),r=(i)=>{if(i.preventDefault(),t==="idle")n("exiting")},o=async(i)=>{if(i.animationName!==e.animation)return;n("gone"),await e.action()},a=t==="exiting"?{"data-animation":e.animation,onAnimationEnd:o}:void 0;return{visible:t!=="gone",attach:a,trigger:r}}import{useMemo as P,useState as F}from"react";var O;((r)=>{r.idle="idle";r.selected="selected";r.error="error"})(O||={});function Re(e,t){let n=t?.maxSizeBytes??Number.POSITIVE_INFINITY,[r,o]=F(0),[a,i]=F("idle"),[l,s]=F(null);function p(f){let x=f.currentTarget.files;if(!x?.[0])return;let E=x[0];if(E.size>n){i("error");return}if(!t.mimeTypes.includes(E.type)){i("error");return}return s(E),i("selected"),E}function y(){o((f)=>f+1),s(null),i("idle")}let T=P(()=>l?URL.createObjectURL(l):void 0,[l]);function A(f){return f.some((x)=>x===a)}let U={actions:{selectFile:p,clearFile:y},input:{props:{id:e,name:e,multiple:!1,accept:t.mimeTypes.join(",")},key:r},label:{props:{htmlFor:e}},matches:A};if(a==="idle")return{data:null,isError:!1,isIdle:!0,isSelected:!1,state:a,...U};if(a==="selected")return{data:l,isError:!1,isIdle:!1,isSelected:!0,preview:T,state:a,...U};return{data:null,isError:!0,isIdle:!1,isSelected:!1,state:a,...U}}import{useCallback as X,useMemo as B,useRef as K}from"react";import{useEffect as W}from"react";import{tinykeys as q}from"tinykeys";function h(e,t){let n=t?.enabled??!0;W(()=>{if(!n)return;let r=q(window,e);return()=>r()},[e,n])}function Le(e){let t=K(null),n=X(()=>{if(t.current)t.current.focus()},[]);return h({[e]:n}),B(()=>({ref:t}),[])}import{useCallback as _,useRef as $}from"react";import{useState as Y}from"react";function v({name:e,defaultValue:t=!1}){let[n,r]=Y(t);return{on:n,off:!n,enable:()=>r(!0),disable:()=>r(!1),toggle:()=>r((p)=>!p),props:{controller:{"aria-expanded":n?"true":"false","aria-controls":e,role:"button",tabIndex:0},target:{id:e,role:"region","aria-hidden":n?"false":"true"}}}}function M(e){let{on:t,off:n,enable:r,disable:o,toggle:a,props:i,...l}=e;return{toggle:{on:t,off:n,enable:r,disable:o,toggle:a,props:i},rest:l}}function Oe(e){let t=e?.enabled??!0,n=v({name:"_internal"}),r=$(null),o=typeof window<"u"&&"PointerEvent"in window?"pointerenter":"mouseenter",a=typeof window<"u"&&"PointerEvent"in window?"pointerleave":"mouseleave";return{attach:{ref:_((l)=>{let s=r.current;if(s)s.removeEventListener(o,n.enable),s.removeEventListener(a,n.disable);if(r.current=l,l&&t)l.addEventListener(o,n.enable),l.addEventListener(a,n.disable)},[o,a,t,n.enable,n.disable])},hovering:n.on&&t}}import{useCallback as J}from"react";function Xe(){return{onKeyDown:J((t)=>{if(!t.metaKey||t.key!=="Enter")return;t.preventDefault(),t.currentTarget.form?.requestSubmit()},[])}}import{useCallback as S,useState as C}from"react";var j;((o)=>{o.idle="idle";o.loading="loading";o.error="error";o.done="done"})(j||={});function Ye(e){let[t,n]=C("idle"),[r,o]=C(null),a=S(()=>{o(null),n("idle")},[]),i=S(async()=>{if(t==="loading")return;o(null),n("loading");try{let s=await e.perform();if(!s.ok){n("error"),o(null),await e.onError?.(null);return}if(n("done"),await e.onSuccess?.(s),e.autoResetDelayMs)setTimeout(()=>n("idle"),e.autoResetDelayMs);return s}catch(s){n("error"),o(s),await e.onError?.(s)}},[t,e]),l=S(async(s)=>{s.preventDefault(),await i()},[i]);return{state:t,error:r,isIdle:t==="idle",isLoading:t==="loading",isError:t==="error",isDone:t==="done",mutate:i,handleSubmit:l,reset:a}}import{useState as V}from"react";class c{static EMPTY=void 0;value=c.EMPTY;constructor(e){this.value=c.isEmpty(e)?c.EMPTY:e}get(){return this.value}isEmpty(){return c.isEmpty(this.value)}static isEmpty(e){return e===void 0||e===null||Number.isNaN(e)}static compare(e,t){if(c.isEmpty(e)&&c.isEmpty(t))return!0;return e===t}}function ze(e){let t=new c(e.defaultValue),[n,r]=V(t.get()),[o,a]=V(c.isEmpty(t.get())?"":String(t.get())),i=(s)=>{let p=new c(s).get();r(p),a(c.isEmpty(p)?"":String(p))},l=(s)=>{let p=s.currentTarget,y=p.value;if(a(y),y==="")return r(c.EMPTY);let T=p.valueAsNumber;if(!Number.isFinite(T))return;if(!s.currentTarget.validity.valid)return;r(T)};return{defaultValue:t.get(),value:n,set:i,handleChange:l,clear:()=>i(t.get()),label:{props:{htmlFor:e.name}},input:{props:{id:e.name,name:e.name,value:o,onChange:l}},changed:!c.compare(n,t.get()),unchanged:c.compare(n,t.get()),empty:c.isEmpty(n)}}import{useEffect as z}from"react";function L(e=!0){z(()=>{if(typeof document>"u")return;let t=document.body.style.overflow;if(e)document.body.style.overflow="hidden";return()=>{document.body.style.overflow=t}},[e])}import{useEffect as Z,useState as G}from"react";class m{static EMPTY=void 0;value=m.EMPTY;constructor(e){this.value=m.isEmpty(e)?m.EMPTY:e}get(){return this.value}isEmpty(){return m.isEmpty(this.value)}static isEmpty(e){return e===void 0||e===""||e===null}static compare(e,t){if(m.isEmpty(e)&&m.isEmpty(t))return!0;return e===t}}function nt(e){let t=new m(e.defaultValue),[n,r]=G(t.get()),o=(i)=>r(new m(i).get()),a=(i)=>o(i.currentTarget.value);return Z(()=>{o(t.get())},[e.defaultValue]),{defaultValue:t.get(),value:n,set:o,handleChange:a,clear:()=>o(t.get()),label:{props:{htmlFor:e.name}},input:{props:{id:e.name,name:e.name,value:m.isEmpty(n)?"":n,onChange:a}},changed:!m.compare(n,t.get()),unchanged:m.compare(n,t.get()),empty:m.isEmpty(n)}}function g(){}import{jsxDEV as te}from"react/jsx-dev-runtime";function ht(e){let{locked:t,...n}=e,r=t??!1,{toggle:o,rest:a}=M(n),i=ee(null);return Q(()=>{if(e.on)i.current?.showModal();else i.current?.close()},[e.on]),h({Escape:r?g:o.disable}),L(e.on),w(i,r?g:o.disable),te("dialog",{ref:i,tabIndex:0,"aria-modal":"true","data-disp":e.on?"flex":"none","data-dir":"column","data-mx":"auto","data-p":"5","data-position":"fixed","data-z":"2","data-bg":"neutral-900","data-br":"xs","data-backdrop":"stronger","data-animation":"grow-fade-in",...a},void 0,!1,void 0,this)}var Rt={email:{inputMode:"email",autoComplete:"email",autoCapitalize:"none",spellCheck:"false"},password:{new:{autoComplete:"new-password"},current:{autoComplete:"current-password"}},off:{autoComplete:"off",spellCheck:!1}};class ne{static async copy(e){let t=e.onSuccess??g;if(!navigator.clipboard)return;await navigator.clipboard.writeText(e.text),t()}}import re from"js-cookie";class oe{static extractFrom(e){return e.headers.get("cookie")??""}static set(e,t){re.set(e,t)}}class ie{static fromRevision(e){return{"if-match":String(e)}}}function Lt(e){return function(){for(let t of e)t()}}class ae{static allUnchanged(e){return e.every((t)=>t.unchanged)}static allEmpty(e){return e.every((t)=>t.empty)}static anyEmpty(e){return e.some((t)=>t.empty)}static anyUnchanged(e){return e.some((t)=>t.unchanged)}static anyChanged(e){return e.some((t)=>t.changed)}}class se{static input(e){let t=e.required??!0;if(e.min&&!e.max)return{pattern:`.{${e.min}}`,required:t};if(e.min&&e.max)return{pattern:`.{${e.min},${e.max}}`,required:t};if(!e.min&&e.max)return{pattern:`.{,${e.max}}`,required:t};return{pattern:void 0,required:t}}static textarea(e){let t=e.required??!0;if(e.min&&!e.max)return{minLength:e.min,required:t};if(e.min&&e.max)return{minLength:e.min,maxLength:e.max,required:t};if(!e.min&&e.max)return{maxLength:e.max,required:t};return{required:t}}static exact(e){let t=e.required??!0;return{pattern:e.text,required:t}}static date={min:{today:()=>new Date().toISOString().split("T")[0],tomorrow:()=>{let e=new Date;return new Date(e.setDate(e.getDate()+1)).toISOString().split("T")[0]},yesterday:()=>{let e=new Date;return new Date(e.setDate(e.getDate()-1)).toISOString().split("T")[0]}},max:{today:()=>new Date().toISOString().split("T")[0],tomorrow:()=>{let e=new Date;return new Date(e.setDate(e.getDate()+1)).toISOString().split("T")[0]},yesterday:()=>{let e=new Date;return new Date(e.setDate(e.getDate()-1)).toISOString().split("T")[0]}}}}function kt(){if(typeof window>"u")return;return window}var Ot=(e)=>[{rel:"preload",as:"style",href:e},{rel:"stylesheet",href:e}],Wt=(e)=>({type:"module",src:e}),qt=[{charSet:"utf-8"},{name:"viewport",content:"width=device-width, initial-scale=1"}];import{polishPlurals as le}from"polish-plurals";function I(e){if(e.language==="en"){let t=e.plural??`${e.singular}s`;if(e.value===1)return e.singular;return t}if(e.language==="pl"){let t=e.value??1;if(t===1)return e.singular;return le(e.singular,String(e.plural),String(e.genitive),t)}return console.warn(`[@bgord/frontend] missing pluralization function for language: ${e.language}.`),e.singular}function Yt(e=12){return{times(t){let n=e*t,r={height:{height:u(n)},minHeight:{minHeight:u(n)},maxHeight:{maxHeight:u(n)},width:{width:u(n)},minWidth:{minWidth:u(n)},maxWidth:{maxWidth:u(n)},square:{height:u(n),width:u(n)}},o={height:{style:{height:u(n)}},minHeight:{style:{minHeight:u(n)}},maxHeight:{style:{maxHeight:u(n)}},width:{style:{width:u(n)}},minWidth:{style:{minWidth:u(n)}},maxWidth:{style:{maxWidth:u(n)}},square:{style:{height:u(n),width:u(n)}}};return{px:u(n),raw:n,style:o,...r}}}}function u(e){return`${e}px`}class pe{static get(){return{"time-zone-offset":new Date().getTimezoneOffset().toString()}}}import{createContext as ue,use as b,useCallback as de}from"react";var R=ue({translations:{},language:"en",supportedLanguages:{en:"en"}});function zt(){let e=b(R);if(e===void 0)throw Error("useTranslations must be used within the TranslationsContext");return de((n,r)=>{let o=e.translations[n];if(!o)return console.warn(`[@bgord/ui] missing translation for key: ${n}`),n;if(!r)return o;return Object.entries(r).reduce((a,[i,l])=>{let s=new RegExp(`{{${i}}}`,"g");return a.replace(s,String(l))},o)},[e.translations])}function ce(){let e=b(R);if(e===void 0)throw Error("useLanguage must be used within the TranslationsContext");return e.language}function Zt(){let e=b(R);if(e===void 0)throw Error("useSupportedLanguages must be used within the TranslationsContext");return e.supportedLanguages}function Gt(){let e=ce();return(t)=>I({...t,language:e})}class me{static fromRevision(e){return{"if-match":`W/${e}`}}}export{zt as useTranslations,v as useToggle,nt as useTextField,Zt as useSupportedLanguages,h as useShortcuts,L as useScrollLock,Gt as usePluralize,ze as useNumberField,Ye as useMutation,Xe as useMetaEnterSubmit,ce as useLanguage,Oe as useHover,Le as useFocusKeyboardShortcut,Re as useFile,Fe as useExitAction,Ee as useDateField,w as useClickOutside,I as pluralize,g as noop,kt as getSafeWindow,M as extractUseToggle,Lt as exec,me as WeakETag,O as UseFileState,R as TranslationsContext,pe as TimeZoneOffset,m as TextField,Yt as Rhythm,c as NumberField,j as MutationState,qt as META,Wt as JS,se as Form,ae as Fields,ie as ETag,ht as Dialog,d as DateField,oe as Cookies,ne as Clipboard,Ot as CSS,Rt as Autocomplete};
|
|
1
|
+
export * from "./components";
|
|
2
|
+
export * from "./hooks";
|
|
3
|
+
export * from "./services";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const Autocomplete = {
|
|
2
|
+
email: { inputMode: "email", autoComplete: "email", autoCapitalize: "none", spellCheck: "false" },
|
|
3
|
+
password: { new: { autoComplete: "new-password" }, current: { autoComplete: "current-password" } },
|
|
4
|
+
off: { autoComplete: "off", spellCheck: false },
|
|
5
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class DateField {
|
|
2
|
+
static EMPTY = undefined;
|
|
3
|
+
value = DateField.EMPTY;
|
|
4
|
+
constructor(value) {
|
|
5
|
+
this.value = DateField.isEmpty(value) ? DateField.EMPTY : value;
|
|
6
|
+
}
|
|
7
|
+
get() {
|
|
8
|
+
return this.value;
|
|
9
|
+
}
|
|
10
|
+
isEmpty() {
|
|
11
|
+
return DateField.isEmpty(this.value);
|
|
12
|
+
}
|
|
13
|
+
static isEmpty(value) {
|
|
14
|
+
return value === undefined || value === null || value === "";
|
|
15
|
+
}
|
|
16
|
+
static compare(one, another) {
|
|
17
|
+
if (DateField.isEmpty(one) && DateField.isEmpty(another))
|
|
18
|
+
return true;
|
|
19
|
+
return one === another;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export class Fields {
|
|
2
|
+
static allUnchanged(fields) {
|
|
3
|
+
return fields.every((field) => field.unchanged);
|
|
4
|
+
}
|
|
5
|
+
static allEmpty(fields) {
|
|
6
|
+
return fields.every((field) => field.empty);
|
|
7
|
+
}
|
|
8
|
+
static anyEmpty(fields) {
|
|
9
|
+
return fields.some((field) => field.empty);
|
|
10
|
+
}
|
|
11
|
+
static anyUnchanged(fields) {
|
|
12
|
+
return fields.some((field) => field.unchanged);
|
|
13
|
+
}
|
|
14
|
+
static anyChanged(fields) {
|
|
15
|
+
return fields.some((field) => field.changed);
|
|
16
|
+
}
|
|
17
|
+
}
|
package/dist/services/form.d.ts
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export class Form {
|
|
2
|
+
static input(config) {
|
|
3
|
+
const required = config.required ?? true;
|
|
4
|
+
if (config.min && !config.max)
|
|
5
|
+
return { pattern: `.{${config.min}}`, required };
|
|
6
|
+
if (config.min && config.max)
|
|
7
|
+
return { pattern: `.{${config.min},${config.max}}`, required };
|
|
8
|
+
if (!config.min && config.max)
|
|
9
|
+
return { pattern: `.{,${config.max}}`, required };
|
|
10
|
+
return { pattern: undefined, required };
|
|
11
|
+
}
|
|
12
|
+
static textarea(config) {
|
|
13
|
+
const required = config.required ?? true;
|
|
14
|
+
if (config.min && !config.max)
|
|
15
|
+
return { minLength: config.min, required };
|
|
16
|
+
if (config.min && config.max)
|
|
17
|
+
return { minLength: config.min, maxLength: config.max, required };
|
|
18
|
+
if (!config.min && config.max)
|
|
19
|
+
return { maxLength: config.max, required };
|
|
20
|
+
return { required };
|
|
21
|
+
}
|
|
22
|
+
static exact(config) {
|
|
23
|
+
const required = config.required ?? true;
|
|
24
|
+
return { pattern: config.text, required };
|
|
25
|
+
}
|
|
26
|
+
static date = {
|
|
27
|
+
min: {
|
|
28
|
+
today: () => new Date().toISOString().split("T")[0],
|
|
29
|
+
tomorrow: () => {
|
|
30
|
+
const today = new Date();
|
|
31
|
+
const tomorrow = new Date(today.setDate(today.getDate() + 1));
|
|
32
|
+
return tomorrow.toISOString().split("T")[0];
|
|
33
|
+
},
|
|
34
|
+
yesterday: () => {
|
|
35
|
+
const today = new Date();
|
|
36
|
+
const yesterday = new Date(today.setDate(today.getDate() - 1));
|
|
37
|
+
return yesterday.toISOString().split("T")[0];
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
max: {
|
|
41
|
+
today: () => new Date().toISOString().split("T")[0],
|
|
42
|
+
tomorrow: () => {
|
|
43
|
+
const today = new Date();
|
|
44
|
+
const tomorrow = new Date(today.setDate(today.getDate() + 1));
|
|
45
|
+
return tomorrow.toISOString().split("T")[0];
|
|
46
|
+
},
|
|
47
|
+
yesterday: () => {
|
|
48
|
+
const today = new Date();
|
|
49
|
+
const yesterday = new Date(today.setDate(today.getDate() - 1));
|
|
50
|
+
return yesterday.toISOString().split("T")[0];
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const CSS = (href) => [
|
|
2
|
+
{ rel: "preload", as: "style", href },
|
|
3
|
+
{ rel: "stylesheet", href },
|
|
4
|
+
];
|
|
5
|
+
export const JS = (src) => ({ type: "module", src });
|
|
6
|
+
export const META = [
|
|
7
|
+
{ charSet: "utf-8" },
|
|
8
|
+
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
9
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "./autocomplete";
|
|
2
|
+
export * from "./clipboard";
|
|
3
|
+
export * from "./cookies";
|
|
4
|
+
export * from "./date-field";
|
|
5
|
+
export * from "./etag";
|
|
6
|
+
export * from "./exec";
|
|
7
|
+
export * from "./fields";
|
|
8
|
+
export * from "./form";
|
|
9
|
+
export * from "./get-safe-window";
|
|
10
|
+
export * from "./head";
|
|
11
|
+
export * from "./noop";
|
|
12
|
+
export * from "./number-field";
|
|
13
|
+
export * from "./pluralize";
|
|
14
|
+
export * from "./rhythm";
|
|
15
|
+
export * from "./text-field";
|
|
16
|
+
export * from "./time-zone-offset";
|
|
17
|
+
export * from "./translations";
|
|
18
|
+
export * from "./weak-etag";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function noop() { }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export class NumberField {
|
|
2
|
+
// Empty value is `undefined` here instead of `null`,
|
|
3
|
+
// because HTML elements accept it as an empty value.
|
|
4
|
+
static EMPTY = undefined;
|
|
5
|
+
value = NumberField.EMPTY;
|
|
6
|
+
constructor(value) {
|
|
7
|
+
this.value = NumberField.isEmpty(value) ? NumberField.EMPTY : value;
|
|
8
|
+
}
|
|
9
|
+
get() {
|
|
10
|
+
return this.value;
|
|
11
|
+
}
|
|
12
|
+
isEmpty() {
|
|
13
|
+
return NumberField.isEmpty(this.value);
|
|
14
|
+
}
|
|
15
|
+
static isEmpty(value) {
|
|
16
|
+
return value === undefined || value === null || Number.isNaN(value);
|
|
17
|
+
}
|
|
18
|
+
static compare(one, another) {
|
|
19
|
+
if (NumberField.isEmpty(one) && NumberField.isEmpty(another))
|
|
20
|
+
return true;
|
|
21
|
+
return one === another;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { polishPlurals } from "polish-plurals";
|
|
2
|
+
var PluralizationSupportedLanguages;
|
|
3
|
+
(function (PluralizationSupportedLanguages) {
|
|
4
|
+
PluralizationSupportedLanguages["en"] = "en";
|
|
5
|
+
PluralizationSupportedLanguages["pl"] = "pl";
|
|
6
|
+
})(PluralizationSupportedLanguages || (PluralizationSupportedLanguages = {}));
|
|
7
|
+
export function pluralize(options) {
|
|
8
|
+
if (options.language === PluralizationSupportedLanguages.en) {
|
|
9
|
+
const plural = options.plural ?? `${options.singular}s`;
|
|
10
|
+
if (options.value === 1)
|
|
11
|
+
return options.singular;
|
|
12
|
+
return plural;
|
|
13
|
+
}
|
|
14
|
+
if (options.language === PluralizationSupportedLanguages.pl) {
|
|
15
|
+
const value = options.value ?? 1;
|
|
16
|
+
if (value === 1)
|
|
17
|
+
return options.singular;
|
|
18
|
+
return polishPlurals(options.singular, String(options.plural), String(options.genitive), value);
|
|
19
|
+
}
|
|
20
|
+
// biome-ignore lint: lint/suspicious/noConsole
|
|
21
|
+
console.warn(`[@bgord/frontend] missing pluralization function for language: ${options.language}.`);
|
|
22
|
+
return options.singular;
|
|
23
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const DEFAULT_BASE_PX = 12;
|
|
2
|
+
export function Rhythm(base = DEFAULT_BASE_PX) {
|
|
3
|
+
return {
|
|
4
|
+
times(times) {
|
|
5
|
+
const result = base * times;
|
|
6
|
+
const dimensions = {
|
|
7
|
+
height: { height: px(result) },
|
|
8
|
+
minHeight: { minHeight: px(result) },
|
|
9
|
+
maxHeight: { maxHeight: px(result) },
|
|
10
|
+
width: { width: px(result) },
|
|
11
|
+
minWidth: { minWidth: px(result) },
|
|
12
|
+
maxWidth: { maxWidth: px(result) },
|
|
13
|
+
square: { height: px(result), width: px(result) },
|
|
14
|
+
};
|
|
15
|
+
const style = {
|
|
16
|
+
height: { style: { height: px(result) } },
|
|
17
|
+
minHeight: { style: { minHeight: px(result) } },
|
|
18
|
+
maxHeight: { style: { maxHeight: px(result) } },
|
|
19
|
+
width: { style: { width: px(result) } },
|
|
20
|
+
minWidth: { style: { minWidth: px(result) } },
|
|
21
|
+
maxWidth: { style: { maxWidth: px(result) } },
|
|
22
|
+
square: { style: { height: px(result), width: px(result) } },
|
|
23
|
+
};
|
|
24
|
+
return { px: px(result), raw: result, style, ...dimensions };
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function px(number) {
|
|
29
|
+
return `${number}px`;
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export class TextField {
|
|
2
|
+
// Empty value is `undefined` here instead of `null`,
|
|
3
|
+
// because HTML elements accept it as an empty value.
|
|
4
|
+
static EMPTY = undefined;
|
|
5
|
+
value = TextField.EMPTY;
|
|
6
|
+
constructor(value) {
|
|
7
|
+
this.value = TextField.isEmpty(value) ? TextField.EMPTY : value;
|
|
8
|
+
}
|
|
9
|
+
get() {
|
|
10
|
+
return this.value;
|
|
11
|
+
}
|
|
12
|
+
isEmpty() {
|
|
13
|
+
return TextField.isEmpty(this.value);
|
|
14
|
+
}
|
|
15
|
+
static isEmpty(value) {
|
|
16
|
+
return value === undefined || value === "" || value === null;
|
|
17
|
+
}
|
|
18
|
+
static compare(one, another) {
|
|
19
|
+
if (TextField.isEmpty(one) && TextField.isEmpty(another))
|
|
20
|
+
return true;
|
|
21
|
+
return one === another;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createContext, use, useCallback } from "react";
|
|
2
|
+
import { pluralize } from "./pluralize";
|
|
3
|
+
export const TranslationsContext = createContext({
|
|
4
|
+
translations: {},
|
|
5
|
+
language: "en",
|
|
6
|
+
supportedLanguages: { en: "en" },
|
|
7
|
+
});
|
|
8
|
+
export function useTranslations() {
|
|
9
|
+
const value = use(TranslationsContext);
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
throw new Error("useTranslations must be used within the TranslationsContext");
|
|
12
|
+
const translate = useCallback((key, variables) => {
|
|
13
|
+
const translation = value.translations[key];
|
|
14
|
+
if (!translation) {
|
|
15
|
+
// biome-ignore lint: lint/suspicious/noConsole
|
|
16
|
+
console.warn(`[@bgord/ui] missing translation for key: ${key}`);
|
|
17
|
+
return key;
|
|
18
|
+
}
|
|
19
|
+
if (!variables)
|
|
20
|
+
return translation;
|
|
21
|
+
return Object.entries(variables).reduce((result, [placeholder, value]) => {
|
|
22
|
+
const replacer = new RegExp(`{{${placeholder}}}`, "g");
|
|
23
|
+
return result.replace(replacer, String(value));
|
|
24
|
+
}, translation);
|
|
25
|
+
}, [value.translations]);
|
|
26
|
+
return translate;
|
|
27
|
+
}
|
|
28
|
+
export function useLanguage() {
|
|
29
|
+
const value = use(TranslationsContext);
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
throw new Error("useLanguage must be used within the TranslationsContext");
|
|
32
|
+
return value.language;
|
|
33
|
+
}
|
|
34
|
+
export function useSupportedLanguages() {
|
|
35
|
+
const value = use(TranslationsContext);
|
|
36
|
+
if (value === undefined) {
|
|
37
|
+
throw new Error("useSupportedLanguages must be used within the TranslationsContext");
|
|
38
|
+
}
|
|
39
|
+
return value.supportedLanguages;
|
|
40
|
+
}
|
|
41
|
+
export function usePluralize() {
|
|
42
|
+
const language = useLanguage();
|
|
43
|
+
return (options) => pluralize({ ...options, language });
|
|
44
|
+
}
|