@bgord/ui 0.8.0 → 0.8.2

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.
Files changed (46) hide show
  1. package/dist/components/dialog.d.ts +2 -3
  2. package/dist/components/dialog.js +20 -0
  3. package/dist/components/index.js +1 -0
  4. package/dist/hooks/index.d.ts +1 -0
  5. package/dist/hooks/index.js +13 -0
  6. package/dist/hooks/use-click-outside.js +31 -0
  7. package/dist/hooks/use-date-field.js +36 -0
  8. package/dist/hooks/use-exit-action.d.ts +0 -1
  9. package/dist/hooks/use-exit-action.js +23 -0
  10. package/dist/hooks/use-file.js +52 -0
  11. package/dist/hooks/use-focus-shortcut.js +12 -0
  12. package/dist/hooks/use-hover.js +22 -0
  13. package/dist/hooks/use-meta-enter-submit.js +10 -0
  14. package/dist/hooks/use-mutation.d.ts +26 -0
  15. package/dist/hooks/use-mutation.js +57 -0
  16. package/dist/hooks/use-number-field.js +39 -0
  17. package/dist/hooks/use-scroll-lock.js +13 -0
  18. package/dist/hooks/use-shortcuts.js +11 -0
  19. package/dist/hooks/use-text-field.js +30 -0
  20. package/dist/hooks/use-toggle.js +22 -0
  21. package/dist/index.js +3 -1
  22. package/dist/services/absolute-url.d.ts +1 -0
  23. package/dist/services/absolute-url.js +15 -0
  24. package/dist/services/autocomplete.js +5 -0
  25. package/dist/services/clipboard.js +10 -0
  26. package/dist/services/cookies.js +9 -0
  27. package/dist/services/date-field.js +21 -0
  28. package/dist/services/etag.js +5 -0
  29. package/dist/services/exec.js +6 -0
  30. package/dist/services/fields.js +17 -0
  31. package/dist/services/form.d.ts +0 -1
  32. package/dist/services/form.js +54 -0
  33. package/dist/services/get-safe-window.js +5 -0
  34. package/dist/services/head.js +9 -0
  35. package/dist/services/index.d.ts +1 -0
  36. package/dist/services/index.js +19 -0
  37. package/dist/services/noop.js +1 -0
  38. package/dist/services/number-field.js +23 -0
  39. package/dist/services/pluralize.js +23 -0
  40. package/dist/services/rhythm.js +30 -0
  41. package/dist/services/text-field.js +23 -0
  42. package/dist/services/time-zone-offset.js +5 -0
  43. package/dist/services/translations.js +44 -0
  44. package/dist/services/weak-etag.js +5 -0
  45. package/package.json +1 -1
  46. package/readme.md +2 -0
@@ -1,6 +1,5 @@
1
- import type React from "react";
2
- import * as hooks from "../hooks";
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";
@@ -5,6 +5,7 @@ export * from "./use-file";
5
5
  export * from "./use-focus-shortcut";
6
6
  export * from "./use-hover";
7
7
  export * from "./use-meta-enter-submit";
8
+ export * from "./use-mutation";
8
9
  export * from "./use-number-field";
9
10
  export * from "./use-scroll-lock";
10
11
  export * from "./use-shortcuts";
@@ -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
+ }
@@ -1,4 +1,3 @@
1
- import React from "react";
2
1
  type UseExitActionAnimationType = string;
3
2
  type UseExitActionOptionsType = {
4
3
  action: () => Promise<unknown>;
@@ -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,26 @@
1
+ export declare enum MutationState {
2
+ idle = "idle",
3
+ loading = "loading",
4
+ error = "error",
5
+ done = "done"
6
+ }
7
+ type MutationErrorType = unknown;
8
+ type UseMutationOptions = {
9
+ perform: () => Promise<Response>;
10
+ onSuccess?: (response: Response) => void | Promise<void>;
11
+ onError?: (error: MutationErrorType) => void | Promise<void>;
12
+ autoResetDelayMs?: number;
13
+ };
14
+ type UseMutationReturnType = {
15
+ state: MutationState;
16
+ error: MutationErrorType;
17
+ isIdle: boolean;
18
+ isLoading: boolean;
19
+ isError: boolean;
20
+ isDone: boolean;
21
+ mutate: () => Promise<Response | undefined>;
22
+ handleSubmit: React.FormEventHandler<HTMLFormElement>;
23
+ reset: () => void;
24
+ };
25
+ export declare function useMutation(options: UseMutationOptions): UseMutationReturnType;
26
+ export {};
@@ -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 Z,useRef as G}from"react";import{useEffect as A}from"react";function R(e,t){A(()=>{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:i,right:a,top:s,bottom:l}=o.getBoundingClientRect(),p=r instanceof MouseEvent?r.clientX:r.touches[0].clientX,y=r instanceof MouseEvent?r.clientY:r.touches[0].clientY;if(p>=i&&p<=a&&y>=s&&y<=l)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 w}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===null||e===""}static compare(e,t){if(m.isEmpty(e)&&m.isEmpty(t))return!0;return e===t}}function fe(e){let t=new m(e.defaultValue),[n,r]=w(t.get()),[o,i]=w(m.isEmpty(t.get())?"":t.get()),a=(l)=>{let p=new m(l).get();r(p),i(m.isEmpty(p)?"":String(p))},s=(l)=>{let p=l.currentTarget,y=p.value;if(i(y),y==="")return r(m.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:a,handleChange:s,clear:()=>a(t.get()),label:{props:{htmlFor:e.name}},input:{props:{id:e.name,name:e.name,value:o,onChange:s}},changed:!m.compare(n,t.get()),unchanged:m.compare(n,t.get()),empty:m.isEmpty(n)}}import M from"react";function he(e){let[t,n]=M.useState("idle"),r=(a)=>{if(a.preventDefault(),t==="idle")n("exiting")},o=async(a)=>{if(a.animationName!==e.animation)return;n("gone"),await e.action()},i=t==="exiting"?{"data-animation":e.animation,onAnimationEnd:o}:void 0;return{visible:t!=="gone",attach:i,trigger:r}}import{useMemo as H,useState as F}from"react";var k;((r)=>{r.idle="idle";r.selected="selected";r.error="error"})(k||={});function Fe(e,t){let n=t?.maxSizeBytes??Number.POSITIVE_INFINITY,[r,o]=F(0),[i,a]=F("idle"),[s,l]=F(null);function p(f){let x=f.currentTarget.files;if(!x?.[0])return;let h=x[0];if(h.size>n){a("error");return}if(!t.mimeTypes.includes(h.type)){a("error");return}return l(h),a("selected"),h}function y(){o((f)=>f+1),l(null),a("idle")}let T=H(()=>s?URL.createObjectURL(s):void 0,[s]);function L(f){return f.some((x)=>x===i)}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:L};if(i==="idle")return{data:null,isError:!1,isIdle:!0,isSelected:!1,state:i,...U};if(i==="selected")return{data:s,isError:!1,isIdle:!1,isSelected:!0,preview:T,state:i,...U};return{data:null,isError:!0,isIdle:!1,isSelected:!1,state:i,...U}}import{useCallback as W,useMemo as q,useRef as X}from"react";import{useEffect as O}from"react";import{tinykeys as P}from"tinykeys";function E(e,t){let n=t?.enabled??!0;O(()=>{if(!n)return;let r=P(window,e);return()=>r()},[e,n])}function ve(e){let t=X(null),n=W(()=>{if(t.current)t.current.focus()},[]);return E({[e]:n}),q(()=>({ref:t}),[])}import{useCallback as K,useRef as Y}from"react";import{useState as B}from"react";function D({name:e,defaultValue:t=!1}){let[n,r]=B(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 C(e){let{on:t,off:n,enable:r,disable:o,toggle:i,props:a,...s}=e;return{toggle:{on:t,off:n,enable:r,disable:o,toggle:i,props:a},rest:s}}function He(e){let t=e?.enabled??!0,n=D({name:"_internal"}),r=Y(null),o=typeof window<"u"&&"PointerEvent"in window?"pointerenter":"mouseenter",i=typeof window<"u"&&"PointerEvent"in window?"pointerleave":"mouseleave";return{attach:{ref:K((s)=>{let l=r.current;if(l)l.removeEventListener(o,n.enable),l.removeEventListener(i,n.disable);if(r.current=s,s&&t)s.addEventListener(o,n.enable),s.addEventListener(i,n.disable)},[o,i,t,n.enable,n.disable])},hovering:n.on&&t}}import{useCallback as $}from"react";function Pe(){return{onKeyDown:$((t)=>{if(!t.metaKey||t.key!=="Enter")return;t.preventDefault(),t.currentTarget.form?.requestSubmit()},[])}}import{useState as v}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||Number.isNaN(e)}static compare(e,t){if(d.isEmpty(e)&&d.isEmpty(t))return!0;return e===t}}function Ke(e){let t=new d(e.defaultValue),[n,r]=v(t.get()),[o,i]=v(d.isEmpty(t.get())?"":String(t.get())),a=(l)=>{let p=new d(l).get();r(p),i(d.isEmpty(p)?"":String(p))},s=(l)=>{let p=l.currentTarget,y=p.value;if(i(y),y==="")return r(d.EMPTY);let T=p.valueAsNumber;if(!Number.isFinite(T))return;if(!l.currentTarget.validity.valid)return;r(T)};return{defaultValue:t.get(),value:n,set:a,handleChange:s,clear:()=>a(t.get()),label:{props:{htmlFor:e.name}},input:{props:{id:e.name,name:e.name,value:o,onChange:s}},changed:!d.compare(n,t.get()),unchanged:d.compare(n,t.get()),empty:d.isEmpty(n)}}import{useEffect as J}from"react";function V(e=!0){J(()=>{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 _,useState as j}from"react";class u{static EMPTY=void 0;value=u.EMPTY;constructor(e){this.value=u.isEmpty(e)?u.EMPTY:e}get(){return this.value}isEmpty(){return u.isEmpty(this.value)}static isEmpty(e){return e===void 0||e===""||e===null}static compare(e,t){if(u.isEmpty(e)&&u.isEmpty(t))return!0;return e===t}}function Ze(e){let t=new u(e.defaultValue),[n,r]=j(t.get()),o=(a)=>r(new u(a).get()),i=(a)=>o(a.currentTarget.value);return _(()=>{o(t.get())},[e.defaultValue]),{defaultValue:t.get(),value:n,set:o,handleChange:i,clear:()=>o(t.get()),label:{props:{htmlFor:e.name}},input:{props:{id:e.name,name:e.name,value:u.isEmpty(n)?"":n,onChange:i}},changed:!u.compare(n,t.get()),unchanged:u.compare(n,t.get()),empty:u.isEmpty(n)}}function g(){}import{jsxDEV as N}from"react/jsx-dev-runtime";function ut(e){let t=e.locked??!1,{toggle:n,rest:r}=C(e),o=G(null);return Z(()=>{if(e.on)o.current?.showModal();else o.current?.close()},[e.on]),E({Escape:t?g:n.disable}),V(e.on),R(o,t?g:n.disable),N("dialog",{ref:o,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",...r},void 0,!1,void 0,this)}var xt={email:{inputMode:"email",autoComplete:"email",autoCapitalize:"none",spellCheck:"false"},password:{new:{autoComplete:"new-password"},current:{autoComplete:"current-password"}},off:{autoComplete:"off",spellCheck:!1}};class Q{static async copy(e){let t=e.onSuccess??g;if(!navigator.clipboard)return;await navigator.clipboard.writeText(e.text),t()}}import ee from"js-cookie";class te{static extractFrom(e){return e.headers.get("cookie")??""}static set(e,t){ee.set(e,t)}}class ne{static fromRevision(e){return{"if-match":String(e)}}}function Rt(e){return function(){for(let t of e)t()}}class re{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 oe{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 vt(){if(typeof window>"u")return;return window}var It=(e)=>[{rel:"preload",as:"style",href:e},{rel:"stylesheet",href:e}],Lt=(e)=>({type:"module",src:e}),At=[{charSet:"utf-8"},{name:"viewport",content:"width=device-width, initial-scale=1"}];import{polishPlurals as ie}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 ie(e.singular,String(e.plural),String(e.genitive),t)}return console.warn(`[@bgord/frontend] missing pluralization function for language: ${e.language}.`),e.singular}function Ot(e=12){return{times(t){let n=e*t,r={height:{height:c(n)},minHeight:{minHeight:c(n)},maxHeight:{maxHeight:c(n)},width:{width:c(n)},minWidth:{minWidth:c(n)},maxWidth:{maxWidth:c(n)},square:{height:c(n),width:c(n)}},o={height:{style:{height:c(n)}},minHeight:{style:{minHeight:c(n)}},maxHeight:{style:{maxHeight:c(n)}},width:{style:{width:c(n)}},minWidth:{style:{minWidth:c(n)}},maxWidth:{style:{maxWidth:c(n)}},square:{style:{height:c(n),width:c(n)}}};return{px:c(n),raw:n,style:o,...r}}}}function c(e){return`${e}px`}class ae{static get(){return{"time-zone-offset":new Date().getTimezoneOffset().toString()}}}import{createContext as se,use as b,useCallback as le}from"react";var S=se({translations:{},language:"en",supportedLanguages:{en:"en"}});function Bt(){let e=b(S);if(e===void 0)throw Error("useTranslations must be used within the TranslationsContext");return le((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((i,[a,s])=>{let l=new RegExp(`{{${a}}}`,"g");return i.replace(l,String(s))},o)},[e.translations])}function pe(){let e=b(S);if(e===void 0)throw Error("useLanguage must be used within the TranslationsContext");return e.language}function Kt(){let e=b(S);if(e===void 0)throw Error("useSupportedLanguages must be used within the TranslationsContext");return e.supportedLanguages}function Yt(){let e=pe();return(t)=>I({...t,language:e})}class ce{static fromRevision(e){return{"if-match":`W/${e}`}}}export{Bt as useTranslations,D as useToggle,Ze as useTextField,Kt as useSupportedLanguages,E as useShortcuts,V as useScrollLock,Yt as usePluralize,Ke as useNumberField,Pe as useMetaEnterSubmit,pe as useLanguage,He as useHover,ve as useFocusKeyboardShortcut,Fe as useFile,he as useExitAction,fe as useDateField,R as useClickOutside,I as pluralize,g as noop,vt as getSafeWindow,C as extractUseToggle,Rt as exec,ce as WeakETag,k as UseFileState,S as TranslationsContext,ae as TimeZoneOffset,u as TextField,Ot as Rhythm,d as NumberField,At as META,Lt as JS,oe as Form,re as Fields,ne as ETag,ut as Dialog,m as DateField,te as Cookies,Q as Clipboard,It as CSS,xt as Autocomplete};
1
+ export * from "./components";
2
+ export * from "./hooks";
3
+ export * from "./services";
@@ -0,0 +1 @@
1
+ export declare function absoluteUrl(path: string, request: Request | null): string | URL;
@@ -0,0 +1,15 @@
1
+ export function absoluteUrl(path, request) {
2
+ if (!request)
3
+ return path;
4
+ const incoming = new URL(request.url);
5
+ // Respect reverse-proxy TLS
6
+ const xfproto = request.headers.get("x-forwarded-proto");
7
+ const forwarded = request.headers.get("forwarded");
8
+ const isHttps = xfproto === "https" || (forwarded && /proto=https/i.test(forwarded));
9
+ // Build a clean origin (protocol + host), no path/search/hash
10
+ const origin = `${isHttps ? "https:" : incoming.protocol}//${incoming.host}`;
11
+ // Let WHATWG URL do the right thing:
12
+ // - if `path` is relative with "?...", it becomes pathname + search (NO %3F)
13
+ // - if `path` is absolute, it’s used as-is
14
+ return new URL(path, origin);
15
+ }
@@ -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,10 @@
1
+ import { noop } from "./noop";
2
+ export class Clipboard {
3
+ static async copy(options) {
4
+ const onSuccess = options.onSuccess ?? noop;
5
+ if (!navigator.clipboard)
6
+ return;
7
+ await navigator.clipboard.writeText(options.text);
8
+ onSuccess();
9
+ }
10
+ }
@@ -0,0 +1,9 @@
1
+ import JsCookie from "js-cookie";
2
+ export class Cookies {
3
+ static extractFrom(request) {
4
+ return request.headers.get("cookie") ?? "";
5
+ }
6
+ static set(name, value) {
7
+ JsCookie.set(name, value);
8
+ }
9
+ }
@@ -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,5 @@
1
+ export class ETag {
2
+ static fromRevision(revision) {
3
+ return { "if-match": String(revision) };
4
+ }
5
+ }
@@ -0,0 +1,6 @@
1
+ export function exec(list) {
2
+ return function () {
3
+ for (const fn of list)
4
+ fn();
5
+ };
6
+ }
@@ -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
+ }
@@ -1,4 +1,3 @@
1
- import type React from "react";
2
1
  type PatternTextConfigType = {
3
2
  min?: number;
4
3
  max?: number;
@@ -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,5 @@
1
+ export function getSafeWindow() {
2
+ if (typeof window === "undefined")
3
+ return undefined;
4
+ return window;
5
+ }
@@ -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
+ ];
@@ -1,3 +1,4 @@
1
+ export * from "./absolute-url";
1
2
  export * from "./autocomplete";
2
3
  export * from "./clipboard";
3
4
  export * from "./cookies";
@@ -0,0 +1,19 @@
1
+ export * from "./absolute-url";
2
+ export * from "./autocomplete";
3
+ export * from "./clipboard";
4
+ export * from "./cookies";
5
+ export * from "./date-field";
6
+ export * from "./etag";
7
+ export * from "./exec";
8
+ export * from "./fields";
9
+ export * from "./form";
10
+ export * from "./get-safe-window";
11
+ export * from "./head";
12
+ export * from "./noop";
13
+ export * from "./number-field";
14
+ export * from "./pluralize";
15
+ export * from "./rhythm";
16
+ export * from "./text-field";
17
+ export * from "./time-zone-offset";
18
+ export * from "./translations";
19
+ 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,5 @@
1
+ export class TimeZoneOffset {
2
+ static get() {
3
+ return { "time-zone-offset": new Date().getTimezoneOffset().toString() };
4
+ }
5
+ }
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ export class WeakETag {
2
+ static fromRevision(revision) {
3
+ return { "if-match": `W/${revision}` };
4
+ }
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bgord/ui",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/readme.md CHANGED
@@ -34,12 +34,14 @@ src/
34
34
  │   ├── use-focus-shortcut.ts
35
35
  │   ├── use-hover.ts
36
36
  │   ├── use-meta-enter-submit.ts
37
+ │   ├── use-mutation.ts
37
38
  │   ├── use-number-field.ts
38
39
  │   ├── use-scroll-lock.ts
39
40
  │   ├── use-shortcuts.ts
40
41
  │   ├── use-text-field.ts
41
42
  │   └── use-toggle.ts
42
43
  └── services
44
+ ├── absolute-url.ts
43
45
  ├── autocomplete.ts
44
46
  ├── clipboard.ts
45
47
  ├── cookies.ts