@bgord/ui 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1,2 @@
1
+ export * from "./use-field";
1
2
  export * from "./use-toggle";
@@ -0,0 +1,155 @@
1
+ import { FieldValueAllowedTypes } from "../services/field";
2
+ /** Type for field names */
3
+ type NewFieldNameType = string;
4
+ /** Valid HTML elements that can be used as field inputs */
5
+ export type FieldElementType = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
6
+ /**
7
+ * Defines the strategy for field value persistence
8
+ * @enum {string}
9
+ */
10
+ export declare enum useFieldStrategyEnum {
11
+ /** Store field value in URL parameters */
12
+ params = "params",
13
+ /** Store field value in local state */
14
+ local = "local"
15
+ }
16
+ /**
17
+ * Configuration options for the useField hook
18
+ * @template T - Type of the field value
19
+ */
20
+ export type useFieldConfigType<T extends FieldValueAllowedTypes> = {
21
+ /** Unique identifier for the field */
22
+ name: NewFieldNameType;
23
+ /** Initial value for the field */
24
+ defaultValue?: T;
25
+ /** Strategy for value persistence */
26
+ strategy?: useFieldStrategyEnum;
27
+ };
28
+ /**
29
+ * Return type for the useField hook
30
+ * @template T - Type of the field value
31
+ */
32
+ export type useFieldReturnType<T extends FieldValueAllowedTypes> = {
33
+ /** Current persistence strategy */
34
+ strategy: useFieldStrategyEnum;
35
+ /** Initial field value */
36
+ defaultValue: T;
37
+ /** Current field value */
38
+ currentValue: T;
39
+ /** Non-nullable field value, empty string for empty values */
40
+ value: NonNullable<T>;
41
+ /** Function to set field value */
42
+ set: (value: T) => void;
43
+ /** Change event handler for controlled components */
44
+ handleChange: (event: React.ChangeEvent<FieldElementType>) => void;
45
+ /** Reset field to default value */
46
+ clear: () => void;
47
+ /** Props for field label */
48
+ label: {
49
+ props: {
50
+ htmlFor: NewFieldNameType;
51
+ };
52
+ };
53
+ /** Props for field input */
54
+ input: {
55
+ props: {
56
+ id: NewFieldNameType;
57
+ name: NewFieldNameType;
58
+ };
59
+ };
60
+ /** Whether field value differs from default */
61
+ changed: boolean;
62
+ /** Whether field value equals default */
63
+ unchanged: boolean;
64
+ /** Whether field is empty */
65
+ empty: boolean;
66
+ };
67
+ /**
68
+ * Hook for managing form field state with URL parameters or local state
69
+ *
70
+ * @template T - Type of the field value
71
+ * @param {useFieldConfigType<T>} config - Field configuration
72
+ * @returns {useFieldReturnType<T>} Field state and handlers
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * // Using local strategy
77
+ * function NameField() {
78
+ * const field = useField({
79
+ * name: "username",
80
+ * defaultValue: "",
81
+ * strategy: useFieldStrategyEnum.local
82
+ * });
83
+ *
84
+ * return (
85
+ * <div>
86
+ * <label {...field.label.props}>Username:</label>
87
+ * <input
88
+ * {...field.input.props}
89
+ * type="text"
90
+ * value={field.value}
91
+ * onChange={field.handleChange}
92
+ * />
93
+ * </div>
94
+ * );
95
+ * }
96
+ *
97
+ * // Using URL parameters strategy
98
+ * function SearchField() {
99
+ * const field = useField({
100
+ * name: "q",
101
+ * strategy: useFieldStrategyEnum.params
102
+ * });
103
+ *
104
+ * return (
105
+ * <input
106
+ * type="search"
107
+ * {...field.input.props}
108
+ * value={field.value}
109
+ * onChange={field.handleChange}
110
+ * />
111
+ * );
112
+ * }
113
+ * ```
114
+ */
115
+ export declare function useField<T extends FieldValueAllowedTypes>(config: useFieldConfigType<T>): useFieldReturnType<T>;
116
+ /**
117
+ * Utility class for working with multiple fields
118
+ * @static
119
+ */
120
+ export declare class Fields {
121
+ /**
122
+ * Check if all fields are unchanged
123
+ * @param {Array<{unchanged: boolean}>} fields - Array of field states
124
+ * @returns {boolean} True if all fields match their default values
125
+ */
126
+ static allUnchanged(fields: {
127
+ unchanged: boolean;
128
+ }[]): boolean;
129
+ /**
130
+ * Check if any field is unchanged
131
+ * @param {Array<{unchanged: boolean}>} fields - Array of field states
132
+ * @returns {boolean} True if any field matches its default value
133
+ */
134
+ static anyUnchanged(fields: {
135
+ unchanged: boolean;
136
+ }[]): boolean;
137
+ /**
138
+ * Check if any field has changed
139
+ * @param {Array<{changed: boolean}>} fields - Array of field states
140
+ * @returns {boolean} True if any field differs from its default value
141
+ */
142
+ static anyChanged(fields: {
143
+ changed: boolean;
144
+ }[]): boolean;
145
+ }
146
+ /**
147
+ * Utility class for working with local fields
148
+ * @static
149
+ */
150
+ export declare class LocalFields {
151
+ static clearAll(fields: {
152
+ clear: VoidFunction;
153
+ }[]): () => void;
154
+ }
155
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export * as hooks from "./hooks";
2
- export * as Components from "./components";
1
+ export * from "./components";
2
+ export * from "./hooks";
3
+ export * from "./services";
package/dist/index.js CHANGED
@@ -1,25 +1,105 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, {
5
- get: all[name],
6
- enumerable: true,
7
- configurable: true,
8
- set: (newValue) => all[name] = () => newValue
9
- });
10
- };
1
+ // src/components/button.tsx
2
+ import { jsxDEV } from "react/jsx-dev-runtime";
3
+ function Button() {
4
+ return /* @__PURE__ */ jsxDEV("button", {
5
+ type: "button",
6
+ children: "Click"
7
+ }, undefined, false, undefined, this);
8
+ }
9
+ // src/hooks/use-field.ts
10
+ import { useEffect, useState } from "react";
11
+ import { useSearchParams } from "react-router";
12
+
13
+ // src/services/field.ts
14
+ class Field {
15
+ static emptyValue = undefined;
16
+ static isEmpty(value) {
17
+ return value === undefined || value === "" || value === null;
18
+ }
19
+ static compare(one, another) {
20
+ if (Field.isEmpty(one) && Field.isEmpty(another)) {
21
+ return true;
22
+ }
23
+ return one === another;
24
+ }
25
+ value = Field.emptyValue;
26
+ constructor(value) {
27
+ this.value = Field.isEmpty(value) ? Field.emptyValue : value;
28
+ }
29
+ get() {
30
+ return this.value;
31
+ }
32
+ isEmpty() {
33
+ return Field.isEmpty(this.value);
34
+ }
35
+ }
36
+
37
+ // src/hooks/use-field.ts
38
+ var useFieldStrategyEnum;
39
+ ((useFieldStrategyEnum2) => {
40
+ useFieldStrategyEnum2["params"] = "params";
41
+ useFieldStrategyEnum2["local"] = "local";
42
+ })(useFieldStrategyEnum ||= {});
43
+ function useField(config) {
44
+ const strategy = config.strategy ?? "local" /* local */;
45
+ const [params, setParams] = useSearchParams();
46
+ const givenValue = new Field(params.get(config.name));
47
+ const defaultValue = new Field(config.defaultValue);
48
+ const [currentValue, _setCurrentValue] = useState(givenValue.isEmpty() ? defaultValue.get() : givenValue.get());
49
+ const setCurrentValue = (value) => {
50
+ const candidate = new Field(value);
51
+ _setCurrentValue(candidate.get());
52
+ };
53
+ useEffect(() => {
54
+ const current = new Field(currentValue);
55
+ if (strategy === "params" /* params */) {
56
+ if (current.isEmpty()) {
57
+ params.delete(config.name);
58
+ setParams(params);
59
+ } else {
60
+ params.set(config.name, current.get());
61
+ setParams(params);
62
+ }
63
+ }
64
+ if (strategy === "local" /* local */) {}
65
+ }, [currentValue, params, setParams, config.name, strategy]);
66
+ return {
67
+ strategy,
68
+ defaultValue: defaultValue.get(),
69
+ currentValue,
70
+ value: Field.isEmpty(currentValue) ? "" : currentValue,
71
+ set: setCurrentValue,
72
+ handleChange: (event) => setCurrentValue(event.currentTarget.value),
73
+ clear: () => setCurrentValue(defaultValue.get()),
74
+ label: { props: { htmlFor: config.name } },
75
+ input: { props: { id: config.name, name: config.name } },
76
+ changed: !Field.compare(currentValue, defaultValue.get()),
77
+ unchanged: Field.compare(currentValue, defaultValue.get()),
78
+ empty: Field.isEmpty(currentValue)
79
+ };
80
+ }
11
81
 
12
- // src/hooks/index.ts
13
- var exports_hooks = {};
14
- __export(exports_hooks, {
15
- useToggle: () => useToggle,
16
- extractUseToggle: () => extractUseToggle
17
- });
82
+ class Fields {
83
+ static allUnchanged(fields) {
84
+ return fields.every((field) => field.unchanged);
85
+ }
86
+ static anyUnchanged(fields) {
87
+ return fields.some((field) => field.unchanged);
88
+ }
89
+ static anyChanged(fields) {
90
+ return fields.some((field) => field.changed);
91
+ }
92
+ }
18
93
 
94
+ class LocalFields {
95
+ static clearAll(fields) {
96
+ return () => fields.forEach((field) => field.clear());
97
+ }
98
+ }
19
99
  // src/hooks/use-toggle.ts
20
- import { useCallback, useMemo, useState } from "react";
100
+ import { useCallback, useMemo, useState as useState2 } from "react";
21
101
  function useToggle({ name, defaultValue = false }) {
22
- const [on, setIsOn] = useState(defaultValue);
102
+ const [on, setIsOn] = useState2(defaultValue);
23
103
  const enable = useCallback(() => setIsOn(true), []);
24
104
  const disable = useCallback(() => setIsOn(false), []);
25
105
  const toggle = useCallback(() => setIsOn((v) => !v), []);
@@ -42,20 +122,69 @@ function extractUseToggle(_props) {
42
122
  rest
43
123
  };
44
124
  }
45
- // src/components/index.ts
46
- var exports_components = {};
47
- __export(exports_components, {
48
- Button: () => Button
49
- });
50
-
51
- // src/components/button.tsx
52
- import { jsxDEV } from "react/jsx-dev-runtime";
53
- function Button() {
54
- return /* @__PURE__ */ jsxDEV("button", {
55
- children: "Click"
56
- }, undefined, false, undefined, this);
125
+ // src/services/form.ts
126
+ class Form {
127
+ static inputPattern(config) {
128
+ const required = config.required ?? true;
129
+ if (config.min && !config.max)
130
+ return { pattern: `.{${config.min}}`, required };
131
+ if (config.min && config.max)
132
+ return { pattern: `.{${config.min},${config.max}}`, required };
133
+ if (!config.min && config.max)
134
+ return { pattern: `.{,${config.max}}`, required };
135
+ return { pattern: undefined, required };
136
+ }
137
+ static textareaPattern(config) {
138
+ const required = config.required ?? true;
139
+ if (config.min && !config.max)
140
+ return { minLength: config.min, required };
141
+ if (config.min && config.max)
142
+ return { minLength: config.min, maxLength: config.max, required };
143
+ if (!config.min && config.max)
144
+ return { maxLength: config.max, required };
145
+ return { required };
146
+ }
147
+ }
148
+ // src/services/rhythm.ts
149
+ var DEFAULT_BASE_PX = 12;
150
+ function Rhythm(base = DEFAULT_BASE_PX) {
151
+ return {
152
+ times(times) {
153
+ const result = base * times;
154
+ const dimensions = {
155
+ height: { height: px(result) },
156
+ minHeight: { minHeight: px(result) },
157
+ maxHeight: { maxHeight: px(result) },
158
+ width: { width: px(result) },
159
+ minWidth: { minWidth: px(result) },
160
+ maxWidth: { maxWidth: px(result) },
161
+ square: { height: px(result), width: px(result) }
162
+ };
163
+ const style = {
164
+ height: { style: { height: px(result) } },
165
+ minHeight: { style: { minHeight: px(result) } },
166
+ maxHeight: { style: { maxHeight: px(result) } },
167
+ width: { style: { width: px(result) } },
168
+ minWidth: { style: { minWidth: px(result) } },
169
+ maxWidth: { style: { maxWidth: px(result) } },
170
+ square: { style: { height: px(result), width: px(result) } }
171
+ };
172
+ return { px: px(result), raw: result, style, ...dimensions };
173
+ }
174
+ };
175
+ }
176
+ function px(number) {
177
+ return `${number}px`;
57
178
  }
58
179
  export {
59
- exports_hooks as hooks,
60
- exports_components as Components
180
+ useToggle,
181
+ useFieldStrategyEnum,
182
+ useField,
183
+ extractUseToggle,
184
+ Rhythm,
185
+ LocalFields,
186
+ Form,
187
+ Fields,
188
+ Field,
189
+ Button
61
190
  };
@@ -0,0 +1,10 @@
1
+ export type FieldValueAllowedTypes = string | number | undefined | null;
2
+ export declare class Field<T extends FieldValueAllowedTypes> {
3
+ static readonly emptyValue: undefined;
4
+ static isEmpty(value: FieldValueAllowedTypes): boolean;
5
+ static compare(one: FieldValueAllowedTypes, another: FieldValueAllowedTypes): boolean;
6
+ private readonly value;
7
+ constructor(value: FieldValueAllowedTypes);
8
+ get(): T;
9
+ isEmpty(): boolean;
10
+ }
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ type PatternConfigType = {
3
+ min?: number;
4
+ max?: number;
5
+ required?: React.JSX.IntrinsicElements["input"]["required"];
6
+ };
7
+ export declare class Form {
8
+ static inputPattern(config: PatternConfigType): React.ComponentPropsWithoutRef<"input">;
9
+ static textareaPattern(config: PatternConfigType): React.ComponentPropsWithoutRef<"textarea">;
10
+ }
11
+ export {};
@@ -0,0 +1,3 @@
1
+ export * from "./field";
2
+ export * from "./form";
3
+ export * from "./rhythm";
@@ -0,0 +1,69 @@
1
+ type RhythmBaseType = number;
2
+ type RhythmTimesType = number;
3
+ export declare function Rhythm(base?: RhythmBaseType): {
4
+ times(times: RhythmTimesType): {
5
+ height: {
6
+ height: string;
7
+ };
8
+ minHeight: {
9
+ minHeight: string;
10
+ };
11
+ maxHeight: {
12
+ maxHeight: string;
13
+ };
14
+ width: {
15
+ width: string;
16
+ };
17
+ minWidth: {
18
+ minWidth: string;
19
+ };
20
+ maxWidth: {
21
+ maxWidth: string;
22
+ };
23
+ square: {
24
+ height: string;
25
+ width: string;
26
+ };
27
+ px: string;
28
+ raw: number;
29
+ style: {
30
+ height: {
31
+ style: {
32
+ height: string;
33
+ };
34
+ };
35
+ minHeight: {
36
+ style: {
37
+ minHeight: string;
38
+ };
39
+ };
40
+ maxHeight: {
41
+ style: {
42
+ maxHeight: string;
43
+ };
44
+ };
45
+ width: {
46
+ style: {
47
+ width: string;
48
+ };
49
+ };
50
+ minWidth: {
51
+ style: {
52
+ minWidth: string;
53
+ };
54
+ };
55
+ maxWidth: {
56
+ style: {
57
+ maxWidth: string;
58
+ };
59
+ };
60
+ square: {
61
+ style: {
62
+ height: string;
63
+ width: string;
64
+ };
65
+ };
66
+ };
67
+ };
68
+ };
69
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bgord/ui",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -13,10 +13,11 @@
13
13
  ],
14
14
  "peerDependencies": {
15
15
  "react": "19.1.0",
16
- "react-dom": "19.1.0"
16
+ "react-dom": "19.1.0",
17
+ "react-router": "7.6.3"
17
18
  },
18
19
  "scripts": {
19
- "build:js": "bun build src/index.ts --format esm --outdir dist --packages external --external react --external react-dom --external react/jsx-runtime",
20
+ "build:js": "bun build src/index.ts --format esm --outdir dist --packages external --external react --external react-dom --external react/jsx-runtime --external react-router",
20
21
  "build:types": "bunx tsc --emitDeclarationOnly",
21
22
  "build": "bun run build:js && bun run build:types"
22
23
  },
@@ -24,7 +25,21 @@
24
25
  "access": "public"
25
26
  },
26
27
  "devDependencies": {
28
+ "@happy-dom/global-registrator": "18.0.1",
29
+ "@testing-library/dom": "10.4.0",
30
+ "@testing-library/jest-dom": "6.6.3",
31
+ "@testing-library/react": "16.3.0",
32
+ "@testing-library/user-event": "14.6.1",
33
+ "@types/bun": "1.2.18",
27
34
  "@types/react": "19.1.8",
28
- "@types/react-dom": "19.1.6"
35
+ "@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
+ "knip": "5.61.3",
41
+ "lefthook": "1.11.16",
42
+ "only-allow": "1.2.1",
43
+ "shellcheck": "3.1.0"
29
44
  }
30
45
  }