@arqel-dev/fields 0.15.0 → 0.16.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/README.md +2 -2
- package/dist/color.js +3 -1
- package/dist/color.js.map +1 -1
- package/dist/date.js +83 -3
- package/dist/date.js.map +1 -1
- package/dist/file.js +8 -5
- package/dist/file.js.map +1 -1
- package/dist/index.js +138 -36
- package/dist/index.js.map +1 -1
- package/dist/number.js +30 -18
- package/dist/number.js.map +1 -1
- package/dist/register.js +138 -36
- package/dist/register.js.map +1 -1
- package/dist/relationship.js +9 -6
- package/dist/relationship.js.map +1 -1
- package/dist/select.js +5 -1
- package/dist/select.js.map +1 -1
- package/dist/slug.js +4 -2
- package/dist/slug.js.map +1 -1
- package/dist/text.js +3 -1
- package/dist/text.js.map +1 -1
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cn } from '@arqel-dev/ui/utils';
|
|
2
2
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
+
import { useArqelTranslations } from '@arqel-dev/react/utils';
|
|
3
4
|
import { useState, useEffect, useRef } from 'react';
|
|
4
5
|
|
|
5
6
|
// src/shared/styles.ts
|
|
@@ -98,6 +99,7 @@ function ColorInput({
|
|
|
98
99
|
describedBy
|
|
99
100
|
}) {
|
|
100
101
|
const f = field;
|
|
102
|
+
const t = useArqelTranslations();
|
|
101
103
|
const hasError = errors !== void 0 && errors.length > 0;
|
|
102
104
|
const isDisabled = disabled || f.disabled || f.readonly;
|
|
103
105
|
const current = typeof value === "string" && value.length > 0 ? value : "#000000";
|
|
@@ -133,7 +135,7 @@ function ColorInput({
|
|
|
133
135
|
"button",
|
|
134
136
|
{
|
|
135
137
|
type: "button",
|
|
136
|
-
"aria-label":
|
|
138
|
+
"aria-label": t("arqel.fields.color.preset_aria", "Preset :color", { color: preset }),
|
|
137
139
|
disabled: isDisabled,
|
|
138
140
|
onClick: () => onChange(preset),
|
|
139
141
|
className: "h-6 w-6 rounded-full border border-border disabled:cursor-not-allowed disabled:opacity-50",
|
|
@@ -172,6 +174,65 @@ function DateInput({
|
|
|
172
174
|
}
|
|
173
175
|
);
|
|
174
176
|
}
|
|
177
|
+
function hasZone(value) {
|
|
178
|
+
return /[zZ]$|[+-]\d{2}:?\d{2}$/.test(value.trim());
|
|
179
|
+
}
|
|
180
|
+
function toZonedLocalInput(date, timeZone, withSeconds) {
|
|
181
|
+
const options = {
|
|
182
|
+
year: "numeric",
|
|
183
|
+
month: "2-digit",
|
|
184
|
+
day: "2-digit",
|
|
185
|
+
hour: "2-digit",
|
|
186
|
+
minute: "2-digit",
|
|
187
|
+
hour12: false,
|
|
188
|
+
...withSeconds ? { second: "2-digit" } : {}
|
|
189
|
+
};
|
|
190
|
+
let parts;
|
|
191
|
+
try {
|
|
192
|
+
parts = new Intl.DateTimeFormat("en-CA", { ...options, timeZone }).formatToParts(date);
|
|
193
|
+
} catch {
|
|
194
|
+
parts = new Intl.DateTimeFormat("en-CA", options).formatToParts(date);
|
|
195
|
+
}
|
|
196
|
+
const get = (type) => parts.find((p) => p.type === type)?.value ?? "00";
|
|
197
|
+
const hour = get("hour") === "24" ? "00" : get("hour");
|
|
198
|
+
const date_ = `${get("year")}-${get("month")}-${get("day")}`;
|
|
199
|
+
const time = withSeconds ? `${hour}:${get("minute")}:${get("second")}` : `${hour}:${get("minute")}`;
|
|
200
|
+
return `${date_}T${time}`;
|
|
201
|
+
}
|
|
202
|
+
function zonedLocalInputToIso(wallClock, timeZone) {
|
|
203
|
+
const asUtc = /* @__PURE__ */ new Date(`${wallClock}Z`);
|
|
204
|
+
if (Number.isNaN(asUtc.getTime())) return wallClock;
|
|
205
|
+
const offsetMs = zoneOffsetMs(asUtc, timeZone);
|
|
206
|
+
return new Date(asUtc.getTime() - offsetMs).toISOString();
|
|
207
|
+
}
|
|
208
|
+
function zoneOffsetMs(date, timeZone) {
|
|
209
|
+
try {
|
|
210
|
+
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
211
|
+
timeZone,
|
|
212
|
+
year: "numeric",
|
|
213
|
+
month: "2-digit",
|
|
214
|
+
day: "2-digit",
|
|
215
|
+
hour: "2-digit",
|
|
216
|
+
minute: "2-digit",
|
|
217
|
+
second: "2-digit",
|
|
218
|
+
hour12: false
|
|
219
|
+
});
|
|
220
|
+
const parts = dtf.formatToParts(date);
|
|
221
|
+
const get = (type) => Number(parts.find((p) => p.type === type)?.value ?? "0");
|
|
222
|
+
const hour = get("hour") === 24 ? 0 : get("hour");
|
|
223
|
+
const asIfUtc = Date.UTC(
|
|
224
|
+
get("year"),
|
|
225
|
+
get("month") - 1,
|
|
226
|
+
get("day"),
|
|
227
|
+
hour,
|
|
228
|
+
get("minute"),
|
|
229
|
+
get("second")
|
|
230
|
+
);
|
|
231
|
+
return asIfUtc - date.getTime();
|
|
232
|
+
} catch {
|
|
233
|
+
return 0;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
175
236
|
function DateTimeInput({
|
|
176
237
|
field,
|
|
177
238
|
value,
|
|
@@ -183,22 +244,43 @@ function DateTimeInput({
|
|
|
183
244
|
}) {
|
|
184
245
|
const f = field;
|
|
185
246
|
const hasError = errors !== void 0 && errors.length > 0;
|
|
247
|
+
const timeZone = f.props.timezone;
|
|
248
|
+
const withSeconds = f.props.seconds === true;
|
|
249
|
+
const raw = typeof value === "string" ? value : "";
|
|
250
|
+
let display = raw;
|
|
251
|
+
if (timeZone !== void 0 && timeZone !== "" && raw !== "" && hasZone(raw)) {
|
|
252
|
+
const parsed = new Date(raw);
|
|
253
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
254
|
+
display = toZonedLocalInput(parsed, timeZone, withSeconds);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
186
257
|
return /* @__PURE__ */ jsx(
|
|
187
258
|
"input",
|
|
188
259
|
{
|
|
189
260
|
id: inputId,
|
|
190
261
|
type: "datetime-local",
|
|
191
262
|
className: inputClasses,
|
|
192
|
-
value:
|
|
263
|
+
value: display,
|
|
193
264
|
disabled: disabled || f.disabled || f.readonly,
|
|
194
265
|
readOnly: f.readonly === true,
|
|
195
266
|
required: f.required === true,
|
|
196
267
|
min: f.props.minDate,
|
|
197
268
|
max: f.props.maxDate,
|
|
198
|
-
step:
|
|
269
|
+
step: withSeconds ? 1 : void 0,
|
|
199
270
|
"aria-invalid": hasError || void 0,
|
|
200
271
|
"aria-describedby": describedBy,
|
|
201
|
-
onChange: (e) =>
|
|
272
|
+
onChange: (e) => {
|
|
273
|
+
const next = e.target.value;
|
|
274
|
+
if (next === "") {
|
|
275
|
+
onChange(null);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (timeZone !== void 0 && timeZone !== "") {
|
|
279
|
+
onChange(zonedLocalInputToIso(next, timeZone));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
onChange(next);
|
|
283
|
+
}
|
|
202
284
|
}
|
|
203
285
|
);
|
|
204
286
|
}
|
|
@@ -212,6 +294,7 @@ function FileInput({
|
|
|
212
294
|
describedBy
|
|
213
295
|
}) {
|
|
214
296
|
const f = field;
|
|
297
|
+
const t = useArqelTranslations();
|
|
215
298
|
const hasError = errors !== void 0 && errors.length > 0;
|
|
216
299
|
const isDisabled = disabled || f.disabled || f.readonly;
|
|
217
300
|
const [dragOver, setDragOver] = useState(false);
|
|
@@ -224,7 +307,7 @@ function FileInput({
|
|
|
224
307
|
return /* @__PURE__ */ jsxs(
|
|
225
308
|
"section",
|
|
226
309
|
{
|
|
227
|
-
"aria-label": "File upload",
|
|
310
|
+
"aria-label": t("arqel.fields.file.upload", "File upload"),
|
|
228
311
|
onDragOver: (e) => {
|
|
229
312
|
e.preventDefault();
|
|
230
313
|
if (!isDisabled) setDragOver(true);
|
|
@@ -242,9 +325,9 @@ function FileInput({
|
|
|
242
325
|
isDisabled && "opacity-50"
|
|
243
326
|
),
|
|
244
327
|
children: [
|
|
245
|
-
filename ? /* @__PURE__ */ jsx("span", { className: "font-medium", children: filename }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Drag a file here or click to browse" }),
|
|
328
|
+
filename ? /* @__PURE__ */ jsx("span", { className: "font-medium", children: filename }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("arqel.fields.file.drop_hint", "Drag a file here or click to browse") }),
|
|
246
329
|
/* @__PURE__ */ jsxs("label", { className: "cursor-pointer text-xs text-primary hover:underline", children: [
|
|
247
|
-
filename ? "Choose another file" : "Browse",
|
|
330
|
+
filename ? t("arqel.fields.file.choose_another", "Choose another file") : t("arqel.fields.file.browse", "Browse"),
|
|
248
331
|
/* @__PURE__ */ jsx(
|
|
249
332
|
"input",
|
|
250
333
|
{
|
|
@@ -272,6 +355,7 @@ function ImageInput({
|
|
|
272
355
|
describedBy
|
|
273
356
|
}) {
|
|
274
357
|
const f = field;
|
|
358
|
+
const t = useArqelTranslations();
|
|
275
359
|
const hasError = errors !== void 0 && errors.length > 0;
|
|
276
360
|
const isDisabled = disabled || f.disabled || f.readonly;
|
|
277
361
|
const [previewUrl, setPreviewUrl] = useState(null);
|
|
@@ -293,7 +377,7 @@ function ImageInput({
|
|
|
293
377
|
"img",
|
|
294
378
|
{
|
|
295
379
|
src: previewUrl,
|
|
296
|
-
alt: "Preview",
|
|
380
|
+
alt: t("arqel.fields.image.preview_alt", "Preview"),
|
|
297
381
|
className: cn("max-h-40 w-auto rounded-sm border border-border")
|
|
298
382
|
}
|
|
299
383
|
),
|
|
@@ -306,7 +390,7 @@ function ImageInput({
|
|
|
306
390
|
isDisabled && "cursor-not-allowed opacity-50"
|
|
307
391
|
),
|
|
308
392
|
children: [
|
|
309
|
-
previewUrl ? "Replace image" : "Choose image",
|
|
393
|
+
previewUrl ? t("arqel.fields.image.replace", "Replace image") : t("arqel.fields.image.choose", "Choose image"),
|
|
310
394
|
/* @__PURE__ */ jsx(
|
|
311
395
|
"input",
|
|
312
396
|
{
|
|
@@ -350,18 +434,20 @@ function CurrencyInput({
|
|
|
350
434
|
describedBy
|
|
351
435
|
}) {
|
|
352
436
|
const f = field;
|
|
353
|
-
const [
|
|
437
|
+
const [draft, setDraft] = useState(null);
|
|
354
438
|
const hasError = errors !== void 0 && errors.length > 0;
|
|
355
439
|
const isDisabled = disabled || f.disabled || f.readonly;
|
|
356
440
|
const numeric = typeof value === "number" ? value : value === "" || value == null ? null : Number(value);
|
|
357
441
|
const decimals = f.props.decimals ?? 2;
|
|
358
|
-
const
|
|
359
|
-
const
|
|
442
|
+
const decimalSeparator = f.props.decimalSeparator ?? ".";
|
|
443
|
+
const focused = draft !== null;
|
|
444
|
+
const formatted = numeric === null || Number.isNaN(numeric) ? "" : `${f.props.prefix}${formatNumber(numeric, decimals, f.props.thousandsSeparator, decimalSeparator)}${f.props.suffix ?? ""}`;
|
|
445
|
+
const display = focused ? draft : formatted;
|
|
360
446
|
return /* @__PURE__ */ jsx(
|
|
361
447
|
"input",
|
|
362
448
|
{
|
|
363
449
|
id: inputId,
|
|
364
|
-
type:
|
|
450
|
+
type: "text",
|
|
365
451
|
inputMode: "decimal",
|
|
366
452
|
className: inputClasses,
|
|
367
453
|
value: display,
|
|
@@ -369,25 +455,33 @@ function CurrencyInput({
|
|
|
369
455
|
disabled: isDisabled,
|
|
370
456
|
readOnly: f.readonly === true,
|
|
371
457
|
required: f.required === true,
|
|
372
|
-
min: f.props.min,
|
|
373
|
-
max: f.props.max,
|
|
374
|
-
step: f.props.step ?? 10 ** -decimals,
|
|
375
458
|
"aria-invalid": hasError || void 0,
|
|
376
459
|
"aria-describedby": describedBy,
|
|
377
|
-
onFocus: () =>
|
|
378
|
-
onBlur: () =>
|
|
460
|
+
onFocus: () => setDraft(numeric === null ? "" : toEditable(numeric, decimalSeparator)),
|
|
461
|
+
onBlur: () => setDraft(null),
|
|
379
462
|
onChange: (e) => {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
onChange(null);
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
const num = Number(raw);
|
|
386
|
-
onChange(Number.isNaN(num) ? null : num);
|
|
463
|
+
setDraft(e.target.value);
|
|
464
|
+
onChange(parseEditable(e.target.value, decimalSeparator));
|
|
387
465
|
}
|
|
388
466
|
}
|
|
389
467
|
);
|
|
390
468
|
}
|
|
469
|
+
function toEditable(num, decimal) {
|
|
470
|
+
const s = String(num);
|
|
471
|
+
return decimal === "." ? s : s.replace(".", decimal);
|
|
472
|
+
}
|
|
473
|
+
function parseEditable(raw, decimal) {
|
|
474
|
+
if (raw.trim() === "") {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
let normalised = raw;
|
|
478
|
+
if (decimal !== ".") {
|
|
479
|
+
const escaped = decimal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
480
|
+
normalised = raw.replace(new RegExp(`[^0-9${escaped}+-]`, "g"), "").replace(decimal, ".");
|
|
481
|
+
}
|
|
482
|
+
const num = Number(normalised);
|
|
483
|
+
return Number.isNaN(num) ? null : num;
|
|
484
|
+
}
|
|
391
485
|
function formatNumber(num, decimals, thousands, decimal) {
|
|
392
486
|
const fixed = num.toFixed(decimals);
|
|
393
487
|
const [whole, frac] = fixed.split(".");
|
|
@@ -404,6 +498,7 @@ function NumberInput({
|
|
|
404
498
|
describedBy
|
|
405
499
|
}) {
|
|
406
500
|
const f = field;
|
|
501
|
+
const t = useArqelTranslations();
|
|
407
502
|
const hasError = errors !== void 0 && errors.length > 0;
|
|
408
503
|
const isDisabled = disabled || f.disabled || f.readonly;
|
|
409
504
|
const setValue = (raw) => {
|
|
@@ -448,7 +543,7 @@ function NumberInput({
|
|
|
448
543
|
"button",
|
|
449
544
|
{
|
|
450
545
|
type: "button",
|
|
451
|
-
"aria-label": "Increment",
|
|
546
|
+
"aria-label": t("arqel.fields.increment", "Increment"),
|
|
452
547
|
disabled: isDisabled,
|
|
453
548
|
onClick: () => setValue((current ?? 0) + step),
|
|
454
549
|
className: "flex h-1/2 w-8 items-center justify-center text-xs text-muted-foreground hover:text-foreground disabled:opacity-50",
|
|
@@ -459,7 +554,7 @@ function NumberInput({
|
|
|
459
554
|
"button",
|
|
460
555
|
{
|
|
461
556
|
type: "button",
|
|
462
|
-
"aria-label": "Decrement",
|
|
557
|
+
"aria-label": t("arqel.fields.decrement", "Decrement"),
|
|
463
558
|
disabled: isDisabled,
|
|
464
559
|
onClick: () => setValue((current ?? 0) - step),
|
|
465
560
|
className: "flex h-1/2 w-8 items-center justify-center text-xs text-muted-foreground hover:text-foreground disabled:opacity-50",
|
|
@@ -479,6 +574,7 @@ function BelongsToInput({
|
|
|
479
574
|
describedBy
|
|
480
575
|
}) {
|
|
481
576
|
const f = field;
|
|
577
|
+
const t = useArqelTranslations();
|
|
482
578
|
const hasError = errors !== void 0 && errors.length > 0;
|
|
483
579
|
const isDisabled = disabled || f.disabled || f.readonly;
|
|
484
580
|
const [query, setQuery] = useState("");
|
|
@@ -515,7 +611,9 @@ function BelongsToInput({
|
|
|
515
611
|
type: "text",
|
|
516
612
|
className: inputClasses,
|
|
517
613
|
value: open ? query : selectedLabel ?? (value === null || value === void 0 ? "" : String(value)),
|
|
518
|
-
placeholder: f.placeholder ??
|
|
614
|
+
placeholder: f.placeholder ?? t("arqel.fields.belongsto.search", "Search :resource\u2026", {
|
|
615
|
+
resource: f.props.relatedResource
|
|
616
|
+
}),
|
|
519
617
|
disabled: isDisabled,
|
|
520
618
|
readOnly: !(f.readonly === true || f.readonly === void 0),
|
|
521
619
|
"aria-invalid": hasError || void 0,
|
|
@@ -559,13 +657,12 @@ function BelongsToInput({
|
|
|
559
657
|
}
|
|
560
658
|
function HasManyReadonly({ field, value, inputId, describedBy }) {
|
|
561
659
|
const f = field;
|
|
660
|
+
const t = useArqelTranslations();
|
|
562
661
|
const items = Array.isArray(value) ? value : [];
|
|
563
662
|
if (items.length === 0) {
|
|
564
|
-
return /* @__PURE__ */
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
" linked."
|
|
568
|
-
] });
|
|
663
|
+
return /* @__PURE__ */ jsx("p", { id: inputId, "aria-describedby": describedBy, className: "text-sm text-muted-foreground", children: t("arqel.fields.has_many_empty", "No :resource linked.", {
|
|
664
|
+
resource: f.props.relatedResource
|
|
665
|
+
}) });
|
|
569
666
|
}
|
|
570
667
|
return /* @__PURE__ */ jsx(
|
|
571
668
|
"ul",
|
|
@@ -614,6 +711,7 @@ function MultiSelectInput({
|
|
|
614
711
|
describedBy
|
|
615
712
|
}) {
|
|
616
713
|
const f = field;
|
|
714
|
+
const t = useArqelTranslations();
|
|
617
715
|
const hasError = errors !== void 0 && errors.length > 0;
|
|
618
716
|
const options = normaliseOptions(f.props.options);
|
|
619
717
|
const arr = Array.isArray(value) ? value : [];
|
|
@@ -635,7 +733,9 @@ function MultiSelectInput({
|
|
|
635
733
|
"button",
|
|
636
734
|
{
|
|
637
735
|
type: "button",
|
|
638
|
-
"aria-label":
|
|
736
|
+
"aria-label": t("arqel.fields.multiselect.remove", "Remove :label", {
|
|
737
|
+
label: opt?.label ?? String(v)
|
|
738
|
+
}),
|
|
639
739
|
onClick: () => remove(v),
|
|
640
740
|
className: "text-muted-foreground hover:text-foreground",
|
|
641
741
|
children: "\u2715"
|
|
@@ -746,6 +846,7 @@ function SlugInput({
|
|
|
746
846
|
describedBy
|
|
747
847
|
}) {
|
|
748
848
|
const f = field;
|
|
849
|
+
const t = useArqelTranslations();
|
|
749
850
|
const hasError = errors !== void 0 && errors.length > 0;
|
|
750
851
|
return /* @__PURE__ */ jsx(
|
|
751
852
|
"input",
|
|
@@ -754,7 +855,7 @@ function SlugInput({
|
|
|
754
855
|
type: "text",
|
|
755
856
|
className: inputClasses,
|
|
756
857
|
value: typeof value === "string" ? value : "",
|
|
757
|
-
placeholder: f.placeholder ?? "my-resource-slug",
|
|
858
|
+
placeholder: f.placeholder ?? t("arqel.fields.slug.placeholder", "my-resource-slug"),
|
|
758
859
|
disabled: disabled || f.disabled || f.readonly,
|
|
759
860
|
readOnly: f.readonly === true,
|
|
760
861
|
required: f.required === true,
|
|
@@ -809,6 +910,7 @@ function PasswordInput({
|
|
|
809
910
|
describedBy
|
|
810
911
|
}) {
|
|
811
912
|
const f = field;
|
|
913
|
+
const t = useArqelTranslations();
|
|
812
914
|
const [revealed, setRevealed] = useState(false);
|
|
813
915
|
const hasError = errors !== void 0 && errors.length > 0;
|
|
814
916
|
const isDisabled = disabled || f.disabled || f.readonly;
|
|
@@ -834,7 +936,7 @@ function PasswordInput({
|
|
|
834
936
|
"button",
|
|
835
937
|
{
|
|
836
938
|
type: "button",
|
|
837
|
-
"aria-label": revealed ? "Hide password" : "Show password",
|
|
939
|
+
"aria-label": revealed ? t("arqel.fields.password.hide", "Hide password") : t("arqel.fields.password.show", "Show password"),
|
|
838
940
|
"aria-pressed": revealed,
|
|
839
941
|
disabled: isDisabled,
|
|
840
942
|
onClick: () => setRevealed((v) => !v),
|