strapi-plugin-tags-custom-field 1.0.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.
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/admin/TagsInput-BGUydL2t.js +354 -0
- package/dist/admin/TagsInput-CCehyU5Y.mjs +336 -0
- package/dist/admin/en-BhiXf-Vq.mjs +23 -0
- package/dist/admin/en-C_FVtF2u.js +23 -0
- package/dist/admin/index.js +221 -0
- package/dist/admin/index.mjs +221 -0
- package/dist/admin/src/index.d.ts +10 -0
- package/dist/server/index.js +38 -0
- package/dist/server/index.mjs +38 -0
- package/dist/server/src/index.d.ts +22 -0
- package/package.json +89 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rony Freitas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# strapi-plugin-tags-custom-field
|
|
2
|
+
|
|
3
|
+
Strapi 5 plugin that adds a `tags` custom field to edit a list of tags (`array of strings`) and store it as a native JSON array.
|
|
4
|
+
|
|
5
|
+
## What this plugin does
|
|
6
|
+
|
|
7
|
+
- Registers the `tags` custom field on the server (`type: json`).
|
|
8
|
+
- Registers the custom field in the admin panel with a tags input component.
|
|
9
|
+
- Saves tags as JSON array values (e.g. `["news","featured","tech"]`).
|
|
10
|
+
- Uses Strapi Design System components for native admin look and feel.
|
|
11
|
+
- Supports keyboard and clipboard workflows for faster data entry.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Node.js 18+ (recommended: Node.js 20)
|
|
16
|
+
- Strapi 5
|
|
17
|
+
|
|
18
|
+
## Local plugin development
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install
|
|
22
|
+
npm run build
|
|
23
|
+
npm run test:ts
|
|
24
|
+
npm run verify
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Install from npm (recommended)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install strapi-plugin-tags-custom-field
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then restart your Strapi server and add the field in Content-Type Builder:
|
|
34
|
+
|
|
35
|
+
- Add a new field.
|
|
36
|
+
- Open the Custom fields category.
|
|
37
|
+
- Select `Tags`.
|
|
38
|
+
- Configure the custom field options if needed (see below).
|
|
39
|
+
|
|
40
|
+
## Link locally during development
|
|
41
|
+
|
|
42
|
+
1. In the plugin project:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install
|
|
46
|
+
npm run watch:link
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
2. In the Strapi project (in another terminal):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx yalc add --link strapi-plugin-tags-custom-field
|
|
53
|
+
npm install
|
|
54
|
+
npm run develop
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
3. In the Strapi Content-Type Builder, add the `Tags` custom field.
|
|
58
|
+
|
|
59
|
+
## Input behavior and UX
|
|
60
|
+
|
|
61
|
+
- Press `Enter` to add the current tag.
|
|
62
|
+
- Press the configured separator (default: `,`) to add the current tag.
|
|
63
|
+
- Paste multiple tags at once (separated by newline or the configured separator).
|
|
64
|
+
- Press `Backspace` on an empty input to remove the last tag.
|
|
65
|
+
- Each draft tag has a configurable character limit with a live counter in the UI.
|
|
66
|
+
|
|
67
|
+
## Custom field options (Content-Type Builder)
|
|
68
|
+
|
|
69
|
+
- `maxTags` (number, default: `20`): maximum number of tags allowed.
|
|
70
|
+
- `maxTagLength` (number, default: `40`): maximum number of characters per tag.
|
|
71
|
+
- `allowDuplicates` (boolean, default: `false`): allows repeated tags.
|
|
72
|
+
- `separator` (text, default: `,`): character used for splitting input/paste.
|
|
73
|
+
- `normalizeCase` (select, default: `none`): `none`, `lowercase`, or `UPPERCASE`.
|
|
74
|
+
|
|
75
|
+
## Database value format
|
|
76
|
+
|
|
77
|
+
The value is stored as a native JSON array. Examples:
|
|
78
|
+
|
|
79
|
+
- No tags: `[]`
|
|
80
|
+
- With tags: `["javascript","strapi","cms"]`
|
|
81
|
+
|
|
82
|
+
## Main structure
|
|
83
|
+
|
|
84
|
+
- `server/src/register.ts`: backend custom field registration.
|
|
85
|
+
- `admin/src/index.ts`: admin custom field registration.
|
|
86
|
+
- `admin/src/components/TagsInput.tsx`: visual input with add/remove tag behavior.
|
|
87
|
+
|
|
88
|
+
## Release checklist
|
|
89
|
+
|
|
90
|
+
1. Update version in `package.json`.
|
|
91
|
+
2. Run:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm ci
|
|
95
|
+
npm run release:check
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
3. Publish to npm:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm publish
|
|
102
|
+
```
|
|
103
|
+
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
+
const React = require("react");
|
|
5
|
+
const designSystem = require("@strapi/design-system");
|
|
6
|
+
const icons = require("@strapi/icons");
|
|
7
|
+
const reactIntl = require("react-intl");
|
|
8
|
+
function _interopNamespace(e) {
|
|
9
|
+
if (e && e.__esModule) return e;
|
|
10
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
11
|
+
if (e) {
|
|
12
|
+
for (const k in e) {
|
|
13
|
+
if (k !== "default") {
|
|
14
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
15
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
get: () => e[k]
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
n.default = e;
|
|
23
|
+
return Object.freeze(n);
|
|
24
|
+
}
|
|
25
|
+
const React__namespace = /* @__PURE__ */ _interopNamespace(React);
|
|
26
|
+
const DEFAULT_SEPARATOR = ",";
|
|
27
|
+
const DEFAULT_MAX_TAGS = 20;
|
|
28
|
+
const DEFAULT_MAX_TAG_LENGTH = 40;
|
|
29
|
+
const parseTagsValue = (value) => {
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
return value.map((item) => String(item).trim()).filter((item) => item.length > 0);
|
|
32
|
+
}
|
|
33
|
+
if (typeof value !== "string") {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const normalized = value.trim();
|
|
37
|
+
if (!normalized) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(normalized);
|
|
42
|
+
if (Array.isArray(parsed)) {
|
|
43
|
+
return parsed.map((item) => String(item).trim()).filter((item) => item.length > 0);
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
return [normalized];
|
|
48
|
+
};
|
|
49
|
+
const serializeTagsValue = (tags) => tags;
|
|
50
|
+
const normalizeCaseValue = (rawValue) => {
|
|
51
|
+
if (rawValue === "lowercase" || rawValue === "uppercase") {
|
|
52
|
+
return rawValue;
|
|
53
|
+
}
|
|
54
|
+
return "none";
|
|
55
|
+
};
|
|
56
|
+
const normalizeTag = (value, normalizeCase) => {
|
|
57
|
+
const cleanedValue = value.trim();
|
|
58
|
+
if (!cleanedValue) {
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
if (normalizeCase === "lowercase") {
|
|
62
|
+
return cleanedValue.toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
if (normalizeCase === "uppercase") {
|
|
65
|
+
return cleanedValue.toUpperCase();
|
|
66
|
+
}
|
|
67
|
+
return cleanedValue;
|
|
68
|
+
};
|
|
69
|
+
const parseBoolean = (value, defaultValue = false) => {
|
|
70
|
+
if (typeof value === "boolean") {
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === "string") {
|
|
74
|
+
return value.toLowerCase() === "true";
|
|
75
|
+
}
|
|
76
|
+
return defaultValue;
|
|
77
|
+
};
|
|
78
|
+
const parsePositiveInt = (value, fallbackValue) => {
|
|
79
|
+
const parsedValue = Number(value);
|
|
80
|
+
if (Number.isFinite(parsedValue) && parsedValue > 0) {
|
|
81
|
+
return Math.floor(parsedValue);
|
|
82
|
+
}
|
|
83
|
+
return fallbackValue;
|
|
84
|
+
};
|
|
85
|
+
const getSplitRegex = (separator) => {
|
|
86
|
+
const characters = Array.from(/* @__PURE__ */ new Set([separator, "\n", "\r"])).filter(
|
|
87
|
+
(character) => character.length > 0
|
|
88
|
+
);
|
|
89
|
+
const escapedCharacters = characters.map((character) => character.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("");
|
|
90
|
+
return new RegExp(`[${escapedCharacters}]+`);
|
|
91
|
+
};
|
|
92
|
+
const parseRawTags = (rawValue, separator, normalizeCase) => rawValue.split(getSplitRegex(separator)).map((tag) => normalizeTag(tag, normalizeCase)).filter((tag) => tag.length > 0);
|
|
93
|
+
const hasSplitCharacters = (value, separator) => [separator, "\n", "\r"].some(
|
|
94
|
+
(character) => character.length > 0 && value.includes(character)
|
|
95
|
+
);
|
|
96
|
+
const TagsInput = React__namespace.forwardRef(
|
|
97
|
+
({
|
|
98
|
+
attribute,
|
|
99
|
+
disabled = false,
|
|
100
|
+
error,
|
|
101
|
+
hint,
|
|
102
|
+
intlLabel,
|
|
103
|
+
name,
|
|
104
|
+
onChange,
|
|
105
|
+
placeholder,
|
|
106
|
+
required = false,
|
|
107
|
+
value
|
|
108
|
+
}, ref) => {
|
|
109
|
+
const { formatMessage } = reactIntl.useIntl();
|
|
110
|
+
const [tags, setTags] = React__namespace.useState(() => parseTagsValue(value));
|
|
111
|
+
const [draft, setDraft] = React__namespace.useState("");
|
|
112
|
+
const [localError, setLocalError] = React__namespace.useState();
|
|
113
|
+
React__namespace.useEffect(() => {
|
|
114
|
+
setTags(parseTagsValue(value));
|
|
115
|
+
}, [value]);
|
|
116
|
+
const fieldType = attribute?.type ?? "json";
|
|
117
|
+
const options = attribute?.options ?? {};
|
|
118
|
+
const separator = typeof options.separator === "string" && options.separator.trim().length > 0 ? options.separator.trim().charAt(0) : DEFAULT_SEPARATOR;
|
|
119
|
+
const maxTags = parsePositiveInt(options.maxTags, DEFAULT_MAX_TAGS);
|
|
120
|
+
const maxTagLength = parsePositiveInt(
|
|
121
|
+
options.maxTagLength,
|
|
122
|
+
DEFAULT_MAX_TAG_LENGTH
|
|
123
|
+
);
|
|
124
|
+
const allowDuplicates = parseBoolean(options.allowDuplicates, false);
|
|
125
|
+
const normalizeCase = normalizeCaseValue(options.normalizeCase);
|
|
126
|
+
const label = intlLabel?.id || intlLabel?.defaultMessage ? formatMessage({
|
|
127
|
+
id: intlLabel?.id ?? `${name}.label`,
|
|
128
|
+
defaultMessage: intlLabel?.defaultMessage ?? "Tags"
|
|
129
|
+
}) : "Tags";
|
|
130
|
+
const hintMessage = hint ?? formatMessage({
|
|
131
|
+
id: "tags-input.hint",
|
|
132
|
+
defaultMessage: "Press Enter or type the separator to add tags. Paste multiple tags at once."
|
|
133
|
+
});
|
|
134
|
+
const shownError = error || localError;
|
|
135
|
+
const emitChange = React__namespace.useCallback(
|
|
136
|
+
(nextTags) => {
|
|
137
|
+
onChange({
|
|
138
|
+
target: {
|
|
139
|
+
name,
|
|
140
|
+
type: fieldType,
|
|
141
|
+
value: serializeTagsValue(nextTags)
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
[fieldType, name, onChange]
|
|
146
|
+
);
|
|
147
|
+
const addTags = React__namespace.useCallback(
|
|
148
|
+
(rawTags) => {
|
|
149
|
+
if (rawTags.length === 0) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
let didChange = false;
|
|
153
|
+
setTags((currentTags) => {
|
|
154
|
+
const nextTags = [...currentTags];
|
|
155
|
+
for (const rawTag of rawTags) {
|
|
156
|
+
if (nextTags.length >= maxTags) {
|
|
157
|
+
setLocalError(
|
|
158
|
+
formatMessage(
|
|
159
|
+
{
|
|
160
|
+
id: "tags-input.error.max-tags",
|
|
161
|
+
defaultMessage: "You can only add up to {maxTags} tags."
|
|
162
|
+
},
|
|
163
|
+
{ maxTags }
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (rawTag.length > maxTagLength) {
|
|
169
|
+
setLocalError(
|
|
170
|
+
formatMessage(
|
|
171
|
+
{
|
|
172
|
+
id: "tags-input.error.max-length",
|
|
173
|
+
defaultMessage: "Each tag must be at most {maxLength} characters."
|
|
174
|
+
},
|
|
175
|
+
{ maxLength: maxTagLength }
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const duplicateIndex = nextTags.findIndex(
|
|
181
|
+
(tag) => tag.toLowerCase() === rawTag.toLowerCase()
|
|
182
|
+
);
|
|
183
|
+
if (duplicateIndex !== -1 && !allowDuplicates) {
|
|
184
|
+
setLocalError(
|
|
185
|
+
formatMessage({
|
|
186
|
+
id: "tags-input.error.duplicate",
|
|
187
|
+
defaultMessage: "Duplicate tags are not allowed."
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
nextTags.push(rawTag);
|
|
193
|
+
didChange = true;
|
|
194
|
+
}
|
|
195
|
+
if (didChange) {
|
|
196
|
+
setLocalError(void 0);
|
|
197
|
+
emitChange(nextTags);
|
|
198
|
+
}
|
|
199
|
+
return didChange ? nextTags : currentTags;
|
|
200
|
+
});
|
|
201
|
+
return didChange;
|
|
202
|
+
},
|
|
203
|
+
[allowDuplicates, emitChange, formatMessage, maxTagLength, maxTags]
|
|
204
|
+
);
|
|
205
|
+
const commitDraft = React__namespace.useCallback(() => {
|
|
206
|
+
const parsedTags = parseRawTags(draft, separator, normalizeCase);
|
|
207
|
+
const added = addTags(parsedTags);
|
|
208
|
+
if (added) {
|
|
209
|
+
setDraft("");
|
|
210
|
+
}
|
|
211
|
+
}, [addTags, draft, normalizeCase, separator]);
|
|
212
|
+
const removeTag = React__namespace.useCallback(
|
|
213
|
+
(index) => {
|
|
214
|
+
setTags((currentTags) => {
|
|
215
|
+
const nextTags = currentTags.filter((_, currentIndex) => currentIndex !== index);
|
|
216
|
+
emitChange(nextTags);
|
|
217
|
+
return nextTags;
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
[emitChange]
|
|
221
|
+
);
|
|
222
|
+
const removeLastTag = React__namespace.useCallback(() => {
|
|
223
|
+
setTags((currentTags) => {
|
|
224
|
+
if (currentTags.length === 0) {
|
|
225
|
+
return currentTags;
|
|
226
|
+
}
|
|
227
|
+
const nextTags = currentTags.slice(0, currentTags.length - 1);
|
|
228
|
+
emitChange(nextTags);
|
|
229
|
+
setLocalError(void 0);
|
|
230
|
+
return nextTags;
|
|
231
|
+
});
|
|
232
|
+
}, [emitChange]);
|
|
233
|
+
const onKeyDown = (event) => {
|
|
234
|
+
if (event.key === "Enter" || event.key === separator) {
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
commitDraft();
|
|
237
|
+
}
|
|
238
|
+
if (event.key === "Backspace" && draft.length === 0 && !disabled) {
|
|
239
|
+
event.preventDefault();
|
|
240
|
+
removeLastTag();
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const onPaste = (event) => {
|
|
244
|
+
const pastedText = event.clipboardData.getData("text");
|
|
245
|
+
if (!pastedText || !hasSplitCharacters(pastedText, separator)) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
const parsedTags = parseRawTags(pastedText, separator, normalizeCase);
|
|
250
|
+
addTags(parsedTags);
|
|
251
|
+
};
|
|
252
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
253
|
+
designSystem.Field.Root,
|
|
254
|
+
{
|
|
255
|
+
id: name,
|
|
256
|
+
name,
|
|
257
|
+
required,
|
|
258
|
+
hint: hintMessage,
|
|
259
|
+
error: shownError,
|
|
260
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
|
|
261
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: label }),
|
|
262
|
+
tags.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
263
|
+
designSystem.Box,
|
|
264
|
+
{
|
|
265
|
+
borderColor: "neutral150",
|
|
266
|
+
borderStyle: "solid",
|
|
267
|
+
borderWidth: "1px",
|
|
268
|
+
borderRadius: "4px",
|
|
269
|
+
padding: 2,
|
|
270
|
+
background: "neutral0",
|
|
271
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { wrap: "wrap", gap: 2, children: tags.map((tag, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
272
|
+
designSystem.Tag,
|
|
273
|
+
{
|
|
274
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(icons.Cross, {}),
|
|
275
|
+
disabled,
|
|
276
|
+
onClick: !disabled ? () => removeTag(index) : void 0,
|
|
277
|
+
label: `Remove ${tag}`,
|
|
278
|
+
children: tag
|
|
279
|
+
},
|
|
280
|
+
`${tag}-${index}`
|
|
281
|
+
)) })
|
|
282
|
+
}
|
|
283
|
+
) : null,
|
|
284
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
285
|
+
designSystem.TextInput,
|
|
286
|
+
{
|
|
287
|
+
id: name,
|
|
288
|
+
ref,
|
|
289
|
+
disabled,
|
|
290
|
+
required: required && tags.length === 0,
|
|
291
|
+
value: draft,
|
|
292
|
+
onChange: (event) => {
|
|
293
|
+
const nextDraft = event.currentTarget.value;
|
|
294
|
+
if (nextDraft.length <= maxTagLength) {
|
|
295
|
+
setDraft(nextDraft);
|
|
296
|
+
if (localError) {
|
|
297
|
+
setLocalError(void 0);
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
setLocalError(
|
|
301
|
+
formatMessage(
|
|
302
|
+
{
|
|
303
|
+
id: "tags-input.error.max-length",
|
|
304
|
+
defaultMessage: "Each tag must be at most {maxLength} characters."
|
|
305
|
+
},
|
|
306
|
+
{ maxLength: maxTagLength }
|
|
307
|
+
)
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
onBlur: commitDraft,
|
|
312
|
+
onKeyDown,
|
|
313
|
+
onPaste,
|
|
314
|
+
placeholder: placeholder ?? formatMessage(
|
|
315
|
+
{
|
|
316
|
+
id: "tags-input.placeholder",
|
|
317
|
+
defaultMessage: "Type a tag and press Enter or {separator} to add it"
|
|
318
|
+
},
|
|
319
|
+
{ separator }
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
),
|
|
323
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", gap: 2, children: [
|
|
324
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: formatMessage(
|
|
325
|
+
{
|
|
326
|
+
id: "tags-input.counter.tags",
|
|
327
|
+
defaultMessage: "{count}/{maxTags} tags"
|
|
328
|
+
},
|
|
329
|
+
{ count: tags.length, maxTags }
|
|
330
|
+
) }),
|
|
331
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
332
|
+
designSystem.Typography,
|
|
333
|
+
{
|
|
334
|
+
variant: "pi",
|
|
335
|
+
textColor: draft.length >= maxTagLength ? "danger600" : "neutral600",
|
|
336
|
+
children: formatMessage(
|
|
337
|
+
{
|
|
338
|
+
id: "tags-input.counter.characters",
|
|
339
|
+
defaultMessage: "{count}/{maxLength} characters"
|
|
340
|
+
},
|
|
341
|
+
{ count: draft.length, maxLength: maxTagLength }
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
] }),
|
|
346
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, {}),
|
|
347
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Error, {})
|
|
348
|
+
] })
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
);
|
|
353
|
+
TagsInput.displayName = "TagsInput";
|
|
354
|
+
exports.TagsInput = TagsInput;
|