@bgord/ui 0.4.0 → 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 {};
@@ -8,7 +8,7 @@ type UseExitActionReturnType = {
8
8
  visible: boolean;
9
9
  trigger: (event: React.MouseEvent) => void;
10
10
  attach: {
11
- "data-exit": UseExitActionAnimationType;
11
+ "data-animation": UseExitActionAnimationType;
12
12
  onAnimationEnd: (event: React.AnimationEvent) => void;
13
13
  } | undefined;
14
14
  };
@@ -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-exit": 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})`;
@@ -176,6 +322,20 @@ function Colorful(color) {
176
322
  };
177
323
  return { ...options, style };
178
324
  }
325
+ // src/services/etag.ts
326
+ class ETag {
327
+ static fromRevision(revision) {
328
+ return { "if-match": String(revision) };
329
+ }
330
+ }
331
+ // src/services/exec.ts
332
+ function exec(list) {
333
+ return function() {
334
+ for (const item of list) {
335
+ item();
336
+ }
337
+ };
338
+ }
179
339
  // src/services/fields.ts
180
340
  class Fields {
181
341
  static allUnchanged(fields) {
@@ -217,23 +377,11 @@ class Form {
217
377
  return { required };
218
378
  }
219
379
  }
220
- // src/services/pluralize.ts
221
- import { polishPlurals } from "polish-plurals";
222
- function pluralize(options) {
223
- if (options.language === "en" /* en */) {
224
- const plural = options.plural ?? `${options.singular}s`;
225
- if (options.value === 1)
226
- return options.singular;
227
- return plural;
228
- }
229
- if (options.language === "pl" /* pl */) {
230
- const value = options.value ?? 1;
231
- if (value === 1)
232
- return options.singular;
233
- return polishPlurals(options.singular, String(options.plural), String(options.genitive), value);
234
- }
235
- console.warn(`[@bgord/frontend] missing pluralization function for language: ${options.language}.`);
236
- return options.singular;
380
+ // src/services/get-safe-window.ts
381
+ function getSafeWindow() {
382
+ if (typeof window === "undefined")
383
+ return;
384
+ return window;
237
385
  }
238
386
  // src/services/rhythm.ts
239
387
  var DEFAULT_BASE_PX = 12;
@@ -266,60 +414,38 @@ function Rhythm(base = DEFAULT_BASE_PX) {
266
414
  function px(number) {
267
415
  return `${number}px`;
268
416
  }
269
- // src/services/translations.tsx
270
- import { createContext, use, useCallback as useCallback2 } from "react";
271
- var TranslationsContext = createContext({
272
- translations: {},
273
- language: "en"
274
- });
275
- function useTranslations() {
276
- const value = use(TranslationsContext);
277
- if (value === undefined) {
278
- throw new Error("useTranslations must be used within the TranslationsContext");
417
+ // src/services/weak-etag.ts
418
+ class WeakETag {
419
+ static fromRevision(revision) {
420
+ return { "if-match": `W/${revision}` };
279
421
  }
280
- const translate = useCallback2((key, variables) => {
281
- const translation = value.translations[key];
282
- if (!translation) {
283
- console.warn(`[@bgord/ui] missing translation for key: ${key}`);
284
- return key;
285
- }
286
- if (!variables)
287
- return translation;
288
- return Object.entries(variables).reduce((result, [placeholder, value2]) => {
289
- const regex = new RegExp(`{{${placeholder}}}`, "g");
290
- return result.replace(regex, String(value2));
291
- }, translation);
292
- }, [value.translations]);
293
- return translate;
294
- }
295
- function useLanguage() {
296
- const value = use(TranslationsContext);
297
- if (value === undefined) {
298
- throw new Error("useLanguage must be used within the TranslationsContext");
299
- }
300
- return value.language;
301
- }
302
- function usePluralize() {
303
- const language = useLanguage();
304
- return (options) => pluralize({ ...options, language });
305
422
  }
306
423
  export {
307
424
  useTranslations,
308
425
  useToggle,
309
426
  usePluralize,
427
+ useMetaEnterSubmit,
428
+ useLanguageSelector,
310
429
  useLanguage,
430
+ useKeyboardShortcuts,
311
431
  useHover,
432
+ useFocusKeyboardShortcut,
312
433
  useFieldStrategyEnum,
313
434
  useField,
314
435
  useExitAction,
436
+ useClientFilter,
315
437
  pluralize,
438
+ getSafeWindow,
316
439
  extractUseToggle,
440
+ exec,
441
+ WeakETag,
317
442
  TranslationsContext,
318
443
  Rhythm,
319
444
  LocalFields,
320
445
  Form,
321
446
  Fields,
322
447
  Field,
448
+ ETag,
323
449
  Colorful,
324
450
  Button
325
451
  };
@@ -0,0 +1,6 @@
1
+ export type ETagValueType = string;
2
+ export declare class ETag {
3
+ static fromRevision(revision: number): {
4
+ "if-match": string;
5
+ };
6
+ }
@@ -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,7 +1,11 @@
1
1
  export * from "./colorful";
2
+ export * from "./etag";
3
+ export * from "./exec";
2
4
  export * from "./field";
3
5
  export * from "./fields";
4
6
  export * from "./form";
7
+ export * from "./get-safe-window";
5
8
  export * from "./pluralize";
6
9
  export * from "./rhythm";
7
10
  export * from "./translations";
11
+ export * from "./weak-etag";
@@ -0,0 +1,6 @@
1
+ export type WeakETagValueType = string;
2
+ export declare class WeakETag {
3
+ static fromRevision(revision: number): {
4
+ "if-match": string;
5
+ };
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bgord/ui",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -25,24 +25,27 @@
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
- "cspell": "9.1.3",
40
+ "cspell": "9.1.5",
40
41
  "knip": "5.61.3",
41
42
  "lefthook": "1.12.2",
42
43
  "only-allow": "1.2.1",
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 ADDED
@@ -0,0 +1,52 @@
1
+ # bgord-ui
2
+
3
+ ## Configuration:
4
+
5
+ Clone the repository
6
+
7
+ ```
8
+ git clone git@github.com:bgord/journal.git --recurse-submodules
9
+ ```
10
+
11
+ Install packages
12
+
13
+ ```
14
+ bun i
15
+ ```
16
+
17
+ Run the tests
18
+
19
+ ```
20
+ ./bgord-scripts/test-run.sh
21
+ ```
22
+
23
+ ## Files:
24
+
25
+ ```
26
+ src/
27
+ ├── components
28
+ │   ├── button.tsx
29
+ ├── hooks
30
+ │   ├── use-client-filter.ts
31
+ │   ├── use-exit-action.ts
32
+ │   ├── use-field.ts
33
+ │   ├── use-focus-shortcut.ts
34
+ │   ├── use-hover.ts
35
+ │   ├── use-language-selector.tsx
36
+ │   ├── use-meta-enter-submit.tsx
37
+ │   ├── use-shortcuts.ts
38
+ │   └── use-toggle.ts
39
+ └── services
40
+ ├── colorful.ts
41
+ ├── etag.ts
42
+ ├── exec.ts
43
+ ├── field.ts
44
+ ├── fields.ts
45
+ ├── form.ts
46
+ ├── get-safe-window.ts
47
+ ├── pluralize.ts
48
+ ├── rhythm.ts
49
+ ├── translations.tsx
50
+ └── weak-etag.ts
51
+ ```
52
+
package/README.md DELETED
@@ -1,15 +0,0 @@
1
- # bgord-ui
2
-
3
- To install dependencies:
4
-
5
- ```bash
6
- bun install
7
- ```
8
-
9
- To run:
10
-
11
- ```bash
12
- bun run index.ts
13
- ```
14
-
15
- This project was created using `bun init` in bun v1.2.17. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.