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