@capyx/components-library 0.0.6 → 0.0.7

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.
@@ -3,16 +3,24 @@ import type { FC } from 'react';
3
3
  * Props for the TagsInput component
4
4
  */
5
5
  export type TagsInputProps = {
6
+ /** Field name — required for form integration and the hidden native input */
7
+ name: string;
6
8
  /** Array of current tag values */
7
- value: string[];
9
+ value?: string[];
8
10
  /** Callback function called when tags change */
9
- onChange: (value: string[]) => void;
11
+ onChange?: (value: string[]) => void;
10
12
  /** Placeholder text shown when no tags are entered (default: "Add tags...") */
11
13
  placeholder?: string;
12
14
  /** Whether the input is disabled */
13
15
  disabled?: boolean;
14
- /** Name attribute for the input field */
15
- name?: string;
16
+ /** Whether at least one tag is required */
17
+ required?: boolean;
18
+ /** Label used in validation error messages (e.g. "Skills is required") */
19
+ label?: string;
20
+ /** Minimum number of tags required */
21
+ minTags?: number;
22
+ /** Maximum number of tags allowed */
23
+ maxTags?: number;
16
24
  };
17
25
  /**
18
26
  * TagsInput - A multi-tag input component using Material-UI Autocomplete
@@ -21,6 +29,14 @@ export type TagsInputProps = {
21
29
  * Automatically trims whitespace and filters empty values. Tags are displayed
22
30
  * as chips with delete functionality.
23
31
  *
32
+ * Supports validation via:
33
+ * - **react-hook-form**: When wrapped in a `FormProvider`, the component
34
+ * registers itself with `Controller` and validates `required`, `minTags`,
35
+ * and `maxTags` rules automatically.
36
+ * - **Native HTML5**: A hidden `<input>` element participates in native
37
+ * constraint validation (`required` attribute), making it compatible with
38
+ * any form library that relies on `checkValidity()` / `reportValidity()`.
39
+ *
24
40
  * @example
25
41
  * ```tsx
26
42
  * <TagsInput
@@ -28,6 +44,9 @@ export type TagsInputProps = {
28
44
  * value={tags}
29
45
  * onChange={setTags}
30
46
  * placeholder="Add skills..."
47
+ * required
48
+ * minTags={1}
49
+ * maxTags={5}
31
50
  * />
32
51
  * ```
33
52
  */
@@ -1 +1 @@
1
- {"version":3,"file":"TagsInput.d.ts","sourceRoot":"","sources":["../../lib/components/TagsInput.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAEhC;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC5B,kCAAkC;IAClC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,gDAAgD;IAChD,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACpC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,SAAS,EAAE,EAAE,CAAC,cAAc,CAiFxC,CAAC"}
1
+ {"version":3,"file":"TagsInput.d.ts","sourceRoot":"","sources":["../../lib/components/TagsInput.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAGhC;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG;IAC5B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACrC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qCAAqC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,SAAS,EAAE,EAAE,CAAC,cAAc,CAuNxC,CAAC"}
@@ -1,6 +1,7 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createElement as _createElement } from "react";
3
- import { Autocomplete, Chip, TextField } from '@mui/material';
3
+ import { Autocomplete, Chip, FormHelperText, TextField } from '@mui/material';
4
+ import { Controller, useFormContext } from 'react-hook-form';
4
5
  /**
5
6
  * TagsInput - A multi-tag input component using Material-UI Autocomplete
6
7
  *
@@ -8,6 +9,14 @@ import { Autocomplete, Chip, TextField } from '@mui/material';
8
9
  * Automatically trims whitespace and filters empty values. Tags are displayed
9
10
  * as chips with delete functionality.
10
11
  *
12
+ * Supports validation via:
13
+ * - **react-hook-form**: When wrapped in a `FormProvider`, the component
14
+ * registers itself with `Controller` and validates `required`, `minTags`,
15
+ * and `maxTags` rules automatically.
16
+ * - **Native HTML5**: A hidden `<input>` element participates in native
17
+ * constraint validation (`required` attribute), making it compatible with
18
+ * any form library that relies on `checkValidity()` / `reportValidity()`.
19
+ *
11
20
  * @example
12
21
  * ```tsx
13
22
  * <TagsInput
@@ -15,53 +24,118 @@ import { Autocomplete, Chip, TextField } from '@mui/material';
15
24
  * value={tags}
16
25
  * onChange={setTags}
17
26
  * placeholder="Add skills..."
27
+ * required
28
+ * minTags={1}
29
+ * maxTags={5}
18
30
  * />
19
31
  * ```
20
32
  */
21
- export const TagsInput = ({ value = [], onChange, placeholder = 'Add tags...', disabled = false, name, }) => {
22
- return (_jsx(Autocomplete, { multiple: true, freeSolo: true, options: [], value: value, onChange: (_event, newValue) => {
23
- // Filter out empty strings and trim whitespace
24
- const cleanedValues = newValue
25
- .map((v) => (typeof v === 'string' ? v.trim() : v))
26
- .filter((v) => v.length > 0);
27
- onChange(cleanedValues);
28
- }, disabled: disabled, renderValue: (tagValue, getTagProps) => tagValue.map((option, index) => (_createElement(Chip, { ...getTagProps({ index }), key: option, label: option, sx: {
29
- backgroundColor: '#212529',
30
- color: '#ffffff',
31
- '& .MuiChip-deleteIcon': {
32
- color: 'rgba(255, 255, 255, 0.7)',
33
+ export const TagsInput = ({ name, value: valueProp = [], onChange, placeholder = 'Add tags...', disabled = false, required = false, label, minTags, maxTags, }) => {
34
+ const formContext = useFormContext();
35
+ const getFieldError = (fieldName) => {
36
+ try {
37
+ const error = formContext?.formState?.errors?.[fieldName];
38
+ return error?.message;
39
+ }
40
+ catch {
41
+ return undefined;
42
+ }
43
+ };
44
+ const fieldLabel = label || 'This field';
45
+ const buildRules = () => ({
46
+ required: required ? `${fieldLabel} is required` : false,
47
+ validate: {
48
+ ...(minTags != null
49
+ ? {
50
+ minTags: (v) => v.length >= minTags ||
51
+ `At least ${minTags} tag${minTags === 1 ? '' : 's'} required`,
52
+ }
53
+ : {}),
54
+ ...(maxTags != null
55
+ ? {
56
+ maxTags: (v) => v.length <= maxTags ||
57
+ `No more than ${maxTags} tag${maxTags === 1 ? '' : 's'} allowed`,
58
+ }
59
+ : {}),
60
+ },
61
+ });
62
+ const cleanValues = (newValue) => {
63
+ const cleaned = newValue
64
+ .map((v) => (typeof v === 'string' ? v.trim() : v))
65
+ .filter((v) => v.length > 0);
66
+ // Enforce maxTags cap
67
+ if (maxTags != null && cleaned.length > maxTags) {
68
+ return cleaned.slice(0, maxTags);
69
+ }
70
+ return cleaned;
71
+ };
72
+ const renderChips = (tagValue, getTagProps) => tagValue.map((option, index) => (_createElement(Chip, { ...getTagProps({ index }), key: option, label: option, sx: {
73
+ backgroundColor: '#212529',
74
+ color: '#ffffff',
75
+ '& .MuiChip-deleteIcon': {
76
+ color: 'rgba(255, 255, 255, 0.7)',
77
+ userSelect: 'none',
78
+ WebkitUserSelect: 'none',
79
+ MozUserSelect: 'none',
80
+ msUserSelect: 'none',
81
+ pointerEvents: 'auto',
82
+ '&::selection': {
83
+ backgroundColor: 'transparent',
84
+ color: 'transparent',
85
+ },
86
+ '& svg': {
33
87
  userSelect: 'none',
34
88
  WebkitUserSelect: 'none',
35
- MozUserSelect: 'none',
36
- msUserSelect: 'none',
37
- pointerEvents: 'auto',
38
- '&::selection': {
39
- backgroundColor: 'transparent',
40
- color: 'transparent',
41
- },
42
- '& svg': {
43
- userSelect: 'none',
44
- WebkitUserSelect: 'none',
45
- pointerEvents: 'none',
46
- },
47
- '&:hover': {
48
- color: '#dc3545',
49
- },
89
+ pointerEvents: 'none',
50
90
  },
51
- } }))), renderInput: (params) => (_jsx(TextField, { ...params, name: name, placeholder: value.length === 0 ? placeholder : undefined, variant: "outlined", size: "small", sx: {
52
- '& .MuiOutlinedInput-root': {
53
- padding: '4px',
54
- minHeight: '38px',
55
- '& fieldset': {
56
- borderColor: '#ced4da',
57
- },
58
- '&:hover fieldset': {
59
- borderColor: '#86b7fe',
60
- },
61
- '&.Mui-focused fieldset': {
62
- borderColor: '#86b7fe',
63
- borderWidth: '1px',
64
- },
91
+ '&:hover': {
92
+ color: '#dc3545',
65
93
  },
66
- } })) }));
94
+ },
95
+ } })));
96
+ const inputSx = (hasError) => ({
97
+ '& .MuiOutlinedInput-root': {
98
+ padding: '4px',
99
+ minHeight: '38px',
100
+ '& fieldset': {
101
+ borderColor: hasError ? '#dc3545' : '#ced4da',
102
+ },
103
+ '&:hover fieldset': {
104
+ borderColor: hasError ? '#dc3545' : '#86b7fe',
105
+ },
106
+ '&.Mui-focused fieldset': {
107
+ borderColor: hasError ? '#dc3545' : '#86b7fe',
108
+ borderWidth: '1px',
109
+ },
110
+ },
111
+ });
112
+ // ── react-hook-form mode ──────────────────────────────────────────────
113
+ if (formContext) {
114
+ const errorMessage = getFieldError(name);
115
+ const hasError = !!errorMessage;
116
+ return (_jsx(Controller, { name: name, control: formContext.control, defaultValue: valueProp, rules: buildRules(), render: ({ field }) => {
117
+ const tags = field.value ?? [];
118
+ return (_jsxs("div", { children: [_jsx(Autocomplete, { multiple: true, freeSolo: true, options: [], value: tags, onChange: (_event, newValue) => {
119
+ const cleaned = cleanValues(newValue);
120
+ field.onChange(cleaned);
121
+ onChange?.(cleaned);
122
+ }, disabled: disabled, renderValue: (tagValue, getTagProps) => renderChips(tagValue, getTagProps), renderInput: (params) => (_jsx(TextField, { ...params, name: name, placeholder: tags.length === 0 ? placeholder : undefined, variant: "outlined", size: "small", error: hasError, sx: inputSx(hasError), onBlur: field.onBlur })) }), hasError && (_jsx(FormHelperText, { error: true, sx: { mx: '14px', mt: '3px' }, children: errorMessage }))] }));
123
+ } }));
124
+ }
125
+ // ── standalone mode ───────────────────────────────────────────────────
126
+ return (_jsxs("div", { children: [_jsx(Autocomplete, { multiple: true, freeSolo: true, options: [], value: valueProp, onChange: (_event, newValue) => {
127
+ const cleaned = cleanValues(newValue);
128
+ onChange?.(cleaned);
129
+ }, disabled: disabled, renderValue: (tagValue, getTagProps) => renderChips(tagValue, getTagProps), renderInput: (params) => (_jsx(TextField, { ...params, name: name, placeholder: valueProp.length === 0 ? placeholder : undefined, variant: "outlined", size: "small", sx: inputSx(false) })) }), _jsx("input", { type: "text", name: `${name}__native`, value: valueProp.join(','), required: required, "aria-hidden": "true", tabIndex: -1, onChange: () => { }, style: {
130
+ display: 'none',
131
+ position: 'absolute',
132
+ width: 0,
133
+ height: 0,
134
+ opacity: 0,
135
+ padding: 0,
136
+ margin: 0,
137
+ border: 'none',
138
+ overflow: 'hidden',
139
+ pointerEvents: 'none',
140
+ } })] }));
67
141
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capyx/components-library",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Capyx Components Library for forms across applications",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -50,38 +50,38 @@
50
50
  "build-storybook": "storybook build"
51
51
  },
52
52
  "devDependencies": {
53
- "@biomejs/biome": "^2.3.11",
54
- "@chromatic-com/storybook": "^5.0.0",
55
- "@storybook/addon-a11y": "^10.1.11",
56
- "@storybook/addon-docs": "^10.1.11",
57
- "@storybook/addon-onboarding": "^10.1.11",
58
- "@storybook/addon-vitest": "^10.1.11",
59
- "@storybook/react-vite": "^10.1.11",
53
+ "@biomejs/biome": "^2.4.3",
54
+ "@chromatic-com/storybook": "^5.0.1",
55
+ "@storybook/addon-a11y": "^10.2.10",
56
+ "@storybook/addon-docs": "^10.2.10",
57
+ "@storybook/addon-onboarding": "^10.2.10",
58
+ "@storybook/addon-vitest": "^10.2.10",
59
+ "@storybook/react-vite": "^10.2.10",
60
60
  "@types/dateformat": "^5.0.3",
61
61
  "@types/lodash.debounce": "^4.0.9",
62
- "@types/node": "^25.0.9",
63
- "@types/react": "^19.2.8",
62
+ "@types/node": "^25.3.0",
63
+ "@types/react": "^19.2.14",
64
64
  "@types/react-autosuggest": "^10.1.11",
65
65
  "@types/react-datepicker": "^7.0.0",
66
- "@vitest/browser-playwright": "^4.0.17",
67
- "@vitest/coverage-v8": "^4.0.17",
68
- "playwright": "^1.57.0",
69
- "storybook": "^10.1.11",
66
+ "@vitest/browser-playwright": "^4.0.18",
67
+ "@vitest/coverage-v8": "^4.0.18",
68
+ "playwright": "^1.58.2",
69
+ "storybook": "^10.2.10",
70
70
  "typescript": "^5.9.3",
71
- "vitest": "^4.0.17"
71
+ "vitest": "^4.0.18"
72
72
  },
73
73
  "dependencies": {
74
74
  "@emotion/styled": "^11.14.1",
75
- "@mui/material": "^7.3.7",
76
- "@mui/x-date-pickers": "^8.25.0",
75
+ "@mui/material": "^7.3.8",
76
+ "@mui/x-date-pickers": "^8.27.0",
77
77
  "bootstrap": "^5.3.8",
78
78
  "dateformat": "^5.0.3",
79
79
  "dayjs": "^1.11.19",
80
80
  "lodash.debounce": "^4.0.8",
81
- "react": "^19.2.3",
81
+ "react": "^19.2.4",
82
82
  "react-autosuggest": "^10.1.0",
83
83
  "react-bootstrap": "^2.10.10",
84
84
  "react-hook-form": "^7.71.1",
85
- "react-quill-new": "^3.7.0"
85
+ "react-quill-new": "^3.8.3"
86
86
  }
87
87
  }