@bgord/ui 0.4.1 → 0.5.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.
@@ -1,4 +1,9 @@
1
+ export * from "./use-client-filter";
1
2
  export * from "./use-exit-action";
2
3
  export * from "./use-field";
4
+ export * from "./use-focus-shortcut";
3
5
  export * from "./use-hover";
6
+ export * from "./use-language-selector";
7
+ export * from "./use-meta-enter-submit";
8
+ export * from "./use-shortcuts";
4
9
  export * from "./use-toggle";
@@ -0,0 +1,20 @@
1
+ import { FieldValueAllowedTypes } from "../services/field";
2
+ import { useFieldConfigType, useFieldReturnType, useFieldStrategyEnum } from "./use-field";
3
+ export type useClientFilterQueryType = string | undefined;
4
+ type useClientFilterConfigType<T extends FieldValueAllowedTypes> = Omit<useFieldConfigType<T>, "strategy"> & {
5
+ enum: {
6
+ [key: string]: useClientFilterQueryType;
7
+ };
8
+ filterFn?: (value: T) => boolean;
9
+ };
10
+ export type useClientFilterReturnType<T extends FieldValueAllowedTypes> = useFieldReturnType<T> & {
11
+ filterFn: NonNullable<useClientFilterConfigType<T>["filterFn"]>;
12
+ options: {
13
+ name: string;
14
+ value: useClientFilterConfigType<T>["enum"][0];
15
+ }[];
16
+ } & {
17
+ strategy: useFieldStrategyEnum.local;
18
+ };
19
+ export declare function useClientFilter<T extends FieldValueAllowedTypes>(config: useClientFilterConfigType<T>): useClientFilterReturnType<T>;
20
+ export {};
@@ -0,0 +1,8 @@
1
+ import { Ref } from "react";
2
+ type FocusableElement = HTMLElement & {
3
+ focus(): void;
4
+ };
5
+ export declare function useFocusKeyboardShortcut<T extends FocusableElement = HTMLInputElement>(shortcut: string): {
6
+ ref: Ref<T>;
7
+ };
8
+ export {};
@@ -0,0 +1,4 @@
1
+ import { useClientFilterReturnType } from "./use-client-filter";
2
+ type LanguageType = string;
3
+ export declare function useLanguageSelector(supportedLanguages: Record<LanguageType, LanguageType>): useClientFilterReturnType<LanguageType>;
4
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare function useMetaEnterSubmit(): {
2
+ onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
3
+ };
@@ -0,0 +1,8 @@
1
+ interface UseKeyboardShortcutsConfigType {
2
+ [keybinding: string]: (event: KeyboardEvent) => void;
3
+ }
4
+ type UseKeyboardShortcutsOptionsType = {
5
+ enabled?: boolean;
6
+ };
7
+ export declare function useKeyboardShortcuts(config: UseKeyboardShortcutsConfigType, options?: UseKeyboardShortcutsOptionsType): void;
8
+ export {};
package/dist/index.js CHANGED
@@ -6,27 +6,8 @@ function Button() {
6
6
  children: "Click"
7
7
  }, undefined, false, undefined, this);
8
8
  }
9
- // src/hooks/use-exit-action.ts
10
- import React from "react";
11
- function useExitAction(options) {
12
- const [phase, setPhase] = React.useState("idle" /* idle */);
13
- const trigger = (event) => {
14
- event.preventDefault();
15
- if (phase === "idle")
16
- setPhase("exiting" /* exiting */);
17
- };
18
- const onAnimationEnd = (event) => {
19
- if (event.animationName !== options.animation)
20
- return;
21
- options.actionFn();
22
- setPhase("gone" /* gone */);
23
- };
24
- const attach = phase === "exiting" ? { "data-animation": options.animation, onAnimationEnd } : undefined;
25
- return { visible: phase !== "gone", attach, trigger };
26
- }
27
- // src/hooks/use-field.ts
28
- import { useEffect, useState } from "react";
29
- import { useSearchParams } from "react-router";
9
+ // src/hooks/use-client-filter.ts
10
+ import { useCallback, useMemo } from "react";
30
11
 
31
12
  // src/services/field.ts
32
13
  class Field {
@@ -53,6 +34,8 @@ class Field {
53
34
  }
54
35
 
55
36
  // src/hooks/use-field.ts
37
+ import { useEffect, useState } from "react";
38
+ import { useSearchParams } from "react-router";
56
39
  var useFieldStrategyEnum;
57
40
  ((useFieldStrategyEnum2) => {
58
41
  useFieldStrategyEnum2["params"] = "params";
@@ -104,8 +87,75 @@ class LocalFields {
104
87
  return () => fields.forEach((field) => field.clear());
105
88
  }
106
89
  }
90
+
91
+ // src/hooks/use-client-filter.ts
92
+ function useClientFilter(config) {
93
+ const query = useField({
94
+ ...config,
95
+ strategy: "local" /* local */
96
+ });
97
+ const defaultFilterFn = useCallback((given) => {
98
+ if (query.empty)
99
+ return true;
100
+ return Field.compare(given, query.currentValue);
101
+ }, [query.empty, query.currentValue]);
102
+ const filterFn = useMemo(() => config.filterFn ?? defaultFilterFn, [config.filterFn, defaultFilterFn]);
103
+ const options = useMemo(() => Object.entries(config.enum).map(([name, value]) => ({ name, value })), [config.enum]);
104
+ return useMemo(() => ({
105
+ ...query,
106
+ filterFn,
107
+ options,
108
+ strategy: "local" /* local */
109
+ }), [query, filterFn, options]);
110
+ }
111
+ // src/hooks/use-exit-action.ts
112
+ import React from "react";
113
+ function useExitAction(options) {
114
+ const [phase, setPhase] = React.useState("idle" /* idle */);
115
+ const trigger = (event) => {
116
+ event.preventDefault();
117
+ if (phase === "idle")
118
+ setPhase("exiting" /* exiting */);
119
+ };
120
+ const onAnimationEnd = (event) => {
121
+ if (event.animationName !== options.animation)
122
+ return;
123
+ options.actionFn();
124
+ setPhase("gone" /* gone */);
125
+ };
126
+ const attach = phase === "exiting" ? { "data-animation": options.animation, onAnimationEnd } : undefined;
127
+ return { visible: phase !== "gone", attach, trigger };
128
+ }
129
+ // src/hooks/use-focus-shortcut.ts
130
+ import { useCallback as useCallback2, useMemo as useMemo3, useRef } from "react";
131
+
132
+ // src/hooks/use-shortcuts.ts
133
+ import { useEffect as useEffect2, useMemo as useMemo2 } from "react";
134
+ import { tinykeys } from "tinykeys";
135
+ function useKeyboardShortcuts(config, options) {
136
+ const enabled = options?.enabled ?? true;
137
+ const memoizedConfig = useMemo2(() => config, [JSON.stringify(Object.keys(config))]);
138
+ useEffect2(() => {
139
+ if (!enabled)
140
+ return;
141
+ const unsubscribe = tinykeys(window, memoizedConfig);
142
+ return () => unsubscribe();
143
+ }, [memoizedConfig, enabled]);
144
+ }
145
+
146
+ // src/hooks/use-focus-shortcut.ts
147
+ function useFocusKeyboardShortcut(shortcut) {
148
+ const ref = useRef(null);
149
+ const handleFocus = useCallback2(() => {
150
+ if (ref.current) {
151
+ ref.current.focus();
152
+ }
153
+ }, []);
154
+ useKeyboardShortcuts({ [shortcut]: handleFocus });
155
+ return useMemo3(() => ({ ref }), []);
156
+ }
107
157
  // src/hooks/use-hover.ts
108
- import { useCallback, useRef } from "react";
158
+ import { useCallback as useCallback3, useRef as useRef2 } from "react";
109
159
 
110
160
  // src/hooks/use-toggle.ts
111
161
  import { useState as useState2 } from "react";
@@ -143,10 +193,10 @@ function useHover({
143
193
  enabled = true
144
194
  } = {}) {
145
195
  const { on: isOn, enable, disable } = useToggle({ name: "is-hovering" });
146
- const nodeRef = useRef(null);
196
+ const nodeRef = useRef2(null);
147
197
  const enterEvent = typeof window !== "undefined" && "PointerEvent" in window ? "pointerenter" : "mouseenter";
148
198
  const leaveEvent = typeof window !== "undefined" && "PointerEvent" in window ? "pointerleave" : "mouseleave";
149
- const ref = useCallback((node) => {
199
+ const ref = useCallback3((node) => {
150
200
  const prev = nodeRef.current;
151
201
  if (prev) {
152
202
  prev.removeEventListener(enterEvent, enable);
@@ -163,6 +213,102 @@ function useHover({
163
213
  isHovering: isOn && enabled
164
214
  };
165
215
  }
216
+ // src/hooks/use-language-selector.tsx
217
+ import Cookies from "js-cookie";
218
+ import { useCallback as useCallback5, useEffect as useEffect3 } from "react";
219
+ import { useRevalidator } from "react-router";
220
+
221
+ // src/services/translations.tsx
222
+ import { createContext, use, useCallback as useCallback4 } from "react";
223
+
224
+ // src/services/pluralize.ts
225
+ import { polishPlurals } from "polish-plurals";
226
+ function pluralize(options) {
227
+ if (options.language === "en" /* en */) {
228
+ const plural = options.plural ?? `${options.singular}s`;
229
+ if (options.value === 1)
230
+ return options.singular;
231
+ return plural;
232
+ }
233
+ if (options.language === "pl" /* pl */) {
234
+ const value = options.value ?? 1;
235
+ if (value === 1)
236
+ return options.singular;
237
+ return polishPlurals(options.singular, String(options.plural), String(options.genitive), value);
238
+ }
239
+ console.warn(`[@bgord/frontend] missing pluralization function for language: ${options.language}.`);
240
+ return options.singular;
241
+ }
242
+
243
+ // src/services/translations.tsx
244
+ var TranslationsContext = createContext({
245
+ translations: {},
246
+ language: "en"
247
+ });
248
+ function useTranslations() {
249
+ const value = use(TranslationsContext);
250
+ if (value === undefined) {
251
+ throw new Error("useTranslations must be used within the TranslationsContext");
252
+ }
253
+ const translate = useCallback4((key, variables) => {
254
+ const translation = value.translations[key];
255
+ if (!translation) {
256
+ console.warn(`[@bgord/ui] missing translation for key: ${key}`);
257
+ return key;
258
+ }
259
+ if (!variables)
260
+ return translation;
261
+ return Object.entries(variables).reduce((result, [placeholder, value2]) => {
262
+ const regex = new RegExp(`{{${placeholder}}}`, "g");
263
+ return result.replace(regex, String(value2));
264
+ }, translation);
265
+ }, [value.translations]);
266
+ return translate;
267
+ }
268
+ function useLanguage() {
269
+ const value = use(TranslationsContext);
270
+ if (value === undefined) {
271
+ throw new Error("useLanguage must be used within the TranslationsContext");
272
+ }
273
+ return value.language;
274
+ }
275
+ function usePluralize() {
276
+ const language = useLanguage();
277
+ return (options) => pluralize({ ...options, language });
278
+ }
279
+
280
+ // src/hooks/use-language-selector.tsx
281
+ function useLanguageSelector(supportedLanguages) {
282
+ const language = useLanguage();
283
+ const revalidator = useRevalidator();
284
+ const field = useClientFilter({
285
+ enum: supportedLanguages,
286
+ defaultValue: language,
287
+ name: "language"
288
+ });
289
+ const handleLanguageChange = useCallback5(() => {
290
+ const current = new Field(field.currentValue);
291
+ if (!current.isEmpty() && field.changed) {
292
+ Cookies.set("language", String(current.get()));
293
+ revalidator.revalidate();
294
+ }
295
+ }, [field.currentValue, field.changed]);
296
+ useEffect3(() => {
297
+ handleLanguageChange();
298
+ }, [handleLanguageChange]);
299
+ return field;
300
+ }
301
+ // src/hooks/use-meta-enter-submit.tsx
302
+ import { useCallback as useCallback6, useMemo as useMemo4 } from "react";
303
+ function useMetaEnterSubmit() {
304
+ const handleMetaEnterSubmit = useCallback6((event) => {
305
+ if (event.key !== "Enter" || !event.metaKey)
306
+ return;
307
+ event.preventDefault();
308
+ event.currentTarget.form?.requestSubmit();
309
+ }, []);
310
+ return useMemo4(() => ({ onKeyDown: handleMetaEnterSubmit }), [handleMetaEnterSubmit]);
311
+ }
166
312
  // src/services/colorful.ts
167
313
  function Colorful(color) {
168
314
  const value = `var(--${color})`;
@@ -182,6 +328,14 @@ class ETag {
182
328
  return { "if-match": String(revision) };
183
329
  }
184
330
  }
331
+ // src/services/exec.ts
332
+ function exec(list) {
333
+ return function() {
334
+ for (const item of list) {
335
+ item();
336
+ }
337
+ };
338
+ }
185
339
  // src/services/fields.ts
186
340
  class Fields {
187
341
  static allUnchanged(fields) {
@@ -223,23 +377,11 @@ class Form {
223
377
  return { required };
224
378
  }
225
379
  }
226
- // src/services/pluralize.ts
227
- import { polishPlurals } from "polish-plurals";
228
- function pluralize(options) {
229
- if (options.language === "en" /* en */) {
230
- const plural = options.plural ?? `${options.singular}s`;
231
- if (options.value === 1)
232
- return options.singular;
233
- return plural;
234
- }
235
- if (options.language === "pl" /* pl */) {
236
- const value = options.value ?? 1;
237
- if (value === 1)
238
- return options.singular;
239
- return polishPlurals(options.singular, String(options.plural), String(options.genitive), value);
240
- }
241
- console.warn(`[@bgord/frontend] missing pluralization function for language: ${options.language}.`);
242
- return options.singular;
380
+ // src/services/get-safe-window.ts
381
+ function getSafeWindow() {
382
+ if (typeof window === "undefined")
383
+ return;
384
+ return window;
243
385
  }
244
386
  // src/services/rhythm.ts
245
387
  var DEFAULT_BASE_PX = 12;
@@ -272,43 +414,6 @@ function Rhythm(base = DEFAULT_BASE_PX) {
272
414
  function px(number) {
273
415
  return `${number}px`;
274
416
  }
275
- // src/services/translations.tsx
276
- import { createContext, use, useCallback as useCallback2 } from "react";
277
- var TranslationsContext = createContext({
278
- translations: {},
279
- language: "en"
280
- });
281
- function useTranslations() {
282
- const value = use(TranslationsContext);
283
- if (value === undefined) {
284
- throw new Error("useTranslations must be used within the TranslationsContext");
285
- }
286
- const translate = useCallback2((key, variables) => {
287
- const translation = value.translations[key];
288
- if (!translation) {
289
- console.warn(`[@bgord/ui] missing translation for key: ${key}`);
290
- return key;
291
- }
292
- if (!variables)
293
- return translation;
294
- return Object.entries(variables).reduce((result, [placeholder, value2]) => {
295
- const regex = new RegExp(`{{${placeholder}}}`, "g");
296
- return result.replace(regex, String(value2));
297
- }, translation);
298
- }, [value.translations]);
299
- return translate;
300
- }
301
- function useLanguage() {
302
- const value = use(TranslationsContext);
303
- if (value === undefined) {
304
- throw new Error("useLanguage must be used within the TranslationsContext");
305
- }
306
- return value.language;
307
- }
308
- function usePluralize() {
309
- const language = useLanguage();
310
- return (options) => pluralize({ ...options, language });
311
- }
312
417
  // src/services/weak-etag.ts
313
418
  class WeakETag {
314
419
  static fromRevision(revision) {
@@ -319,13 +424,20 @@ export {
319
424
  useTranslations,
320
425
  useToggle,
321
426
  usePluralize,
427
+ useMetaEnterSubmit,
428
+ useLanguageSelector,
322
429
  useLanguage,
430
+ useKeyboardShortcuts,
323
431
  useHover,
432
+ useFocusKeyboardShortcut,
324
433
  useFieldStrategyEnum,
325
434
  useField,
326
435
  useExitAction,
436
+ useClientFilter,
327
437
  pluralize,
438
+ getSafeWindow,
328
439
  extractUseToggle,
440
+ exec,
329
441
  WeakETag,
330
442
  TranslationsContext,
331
443
  Rhythm,
@@ -0,0 +1,3 @@
1
+ type ExecFunctionListType = Array<() => void>;
2
+ export declare function exec(list: ExecFunctionListType): () => void;
3
+ export {};
@@ -0,0 +1 @@
1
+ export declare function getSafeWindow(): (Window & typeof globalThis) | undefined;
@@ -1,8 +1,10 @@
1
1
  export * from "./colorful";
2
2
  export * from "./etag";
3
+ export * from "./exec";
3
4
  export * from "./field";
4
5
  export * from "./fields";
5
6
  export * from "./form";
7
+ export * from "./get-safe-window";
6
8
  export * from "./pluralize";
7
9
  export * from "./rhythm";
8
10
  export * from "./translations";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bgord/ui",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -25,17 +25,18 @@
25
25
  "access": "public"
26
26
  },
27
27
  "devDependencies": {
28
+ "@biomejs/biome": "2.0.6",
29
+ "@commitlint/cli": "19.8.1",
30
+ "@commitlint/config-conventional": "19.8.1",
28
31
  "@happy-dom/global-registrator": "18.0.1",
29
32
  "@testing-library/dom": "10.4.0",
30
33
  "@testing-library/jest-dom": "6.6.3",
31
34
  "@testing-library/react": "16.3.0",
32
35
  "@testing-library/user-event": "14.6.1",
33
36
  "@types/bun": "1.2.18",
37
+ "@types/js-cookie": "^3.0.6",
34
38
  "@types/react": "19.1.8",
35
39
  "@types/react-dom": "19.1.6",
36
- "@biomejs/biome": "2.0.6",
37
- "@commitlint/cli": "19.8.1",
38
- "@commitlint/config-conventional": "19.8.1",
39
40
  "cspell": "9.1.5",
40
41
  "knip": "5.61.3",
41
42
  "lefthook": "1.12.2",
@@ -43,6 +44,8 @@
43
44
  "shellcheck": "3.1.0"
44
45
  },
45
46
  "dependencies": {
46
- "polish-plurals": "^1.1.0"
47
+ "js-cookie": "3.0.5",
48
+ "polish-plurals": "1.1.0",
49
+ "tinykeys": "3.0.0"
47
50
  }
48
51
  }
package/readme.md CHANGED
@@ -27,16 +27,23 @@ src/
27
27
  ├── components
28
28
  │   ├── button.tsx
29
29
  ├── hooks
30
+ │   ├── use-client-filter.ts
30
31
  │   ├── use-exit-action.ts
31
32
  │   ├── use-field.ts
33
+ │   ├── use-focus-shortcut.ts
32
34
  │   ├── use-hover.ts
35
+ │   ├── use-language-selector.tsx
36
+ │   ├── use-meta-enter-submit.tsx
37
+ │   ├── use-shortcuts.ts
33
38
  │   └── use-toggle.ts
34
39
  └── services
35
40
  ├── colorful.ts
36
41
  ├── etag.ts
42
+ ├── exec.ts
37
43
  ├── field.ts
38
44
  ├── fields.ts
39
45
  ├── form.ts
46
+ ├── get-safe-window.ts
40
47
  ├── pluralize.ts
41
48
  ├── rhythm.ts
42
49
  ├── translations.tsx