@arqel-dev/fields 0.8.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arqel Contributors
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,42 @@
1
+ # @arqel-dev/fields
2
+
3
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](../../LICENSE)
4
+ [![React](https://img.shields.io/badge/react-%5E19-61dafb.svg)](https://react.dev)
5
+ [![Status](https://img.shields.io/badge/status-pre--alpha-orange.svg)](#)
6
+
7
+ Rich React input components for [Arqel](https://arqel.dev), registered into `@arqel-dev/ui`'s FieldRegistry.
8
+
9
+ ## Status
10
+
11
+ 🚧 **Pre-alpha** — FIELDS-JS-001 + FIELDS-JS-002 entregues (9 components básicos: TextInput, TextareaInput, EmailInput, UrlInput, PasswordInput, NumberInput, CurrencyInput, Checkbox, Toggle).
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pnpm add @arqel-dev/fields @arqel-dev/ui @arqel-dev/react @arqel-dev/types
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```tsx
22
+ // resources/js/app.tsx
23
+ import '@arqel-dev/ui/styles.css';
24
+ import '@arqel-dev/fields/register'; // side effect: registers all built-ins
25
+
26
+ import { createArqelApp } from '@arqel-dev/react/inertia';
27
+
28
+ createArqelApp({ appName: 'Acme', pages: import.meta.glob('./Pages/**/*.tsx') });
29
+ ```
30
+
31
+ ## Subpath imports
32
+
33
+ ```ts
34
+ import { TextInput, EmailInput } from '@arqel-dev/fields/text';
35
+ import { NumberInput, CurrencyInput } from '@arqel-dev/fields/number';
36
+ import { Checkbox, Toggle } from '@arqel-dev/fields/boolean';
37
+ ```
38
+
39
+ ## Links
40
+
41
+ - [Documentação](https://arqel.dev/docs/fields-js) — em construção
42
+ - [PLANNING](../../PLANNING/08-fase-1-mvp.md) — tickets `FIELDS-JS-*`
package/SKILL.md ADDED
@@ -0,0 +1,123 @@
1
+ # SKILL.md — @arqel-dev/fields (JS)
2
+
3
+ > Contexto canónico para AI agents.
4
+
5
+ ## Purpose
6
+
7
+ `@arqel-dev/fields` é o pacote de inputs ricos do lado React. Cada componente registra-se no `FieldRegistry` de `@arqel-dev/ui` (via `import '@arqel-dev/fields/register'`) e substitui os fallbacks nativos do `<FieldRenderer>`. O nome usado em `registerField('TextInput', ...)` corresponde ao retornado por `Field::component()` no PHP — a Resource servidor não precisa saber nada do React.
8
+
9
+ ## Status
10
+
11
+ **Entregue (FIELDS-JS-001..006):**
12
+
13
+ - Pacote `@arqel-dev/fields` com 12 entry points subpath (`./`, `./register`, `./text`, `./number`, `./boolean`, `./select`, `./relationship`, `./date`, `./file`, `./slug`, `./color`, `./hidden`)
14
+ - **21 components no total** — um por field type canônico:
15
+ - **text/**: `TextInput`, `TextareaInput`, `EmailInput`, `UrlInput`, `PasswordInput` (toggle reveal com `aria-pressed`)
16
+ - **number/**: `NumberInput` (stepper buttons), `CurrencyInput` (Intl-format on blur, raw on focus)
17
+ - **boolean/**: `Checkbox`, `Toggle` (role=switch + iOS track/thumb)
18
+ - **select/**: `SelectInput` (native), `MultiSelectInput` (chips removíveis + native multiple), `RadioGroup` (role=radiogroup + inline/stacked)
19
+ - **relationship/**: `BelongsToInput` (async combobox via fetch + 300ms debounce ao `field.props.searchRoute`, role=combobox/listbox), `HasManyReadonly` (lista flat readonly)
20
+ - **date/**: `DateInput` (`type="date"` nativo + min/max), `DateTimeInput` (`type="datetime-local"` + step se `seconds`)
21
+ - **file/**: `FileInput` (drag-drop + section semântica, armazena `File` no form state), `ImageInput` (preview via `URL.createObjectURL`, sem crop em Phase 1)
22
+ - **slug/**: `SlugInput` (normaliza para `[a-z0-9-]+` no keystroke + helper `slugify`)
23
+ - **color/**: `ColorInput` (native picker + presets + hex text input)
24
+ - **hidden/**: `HiddenInput` (`type="hidden"` value carrier)
25
+ - Side-effect `register.ts` registra todos os 21 no FieldRegistry
26
+ - `getRegisteredFields()` (re-exportado de `@arqel-dev/ui/form`) retorna nomes registrados ordenados
27
+ - 23 testes Vitest passando — registry roundtrip, behaviour + a11y de cada componente
28
+ - Estilo via CSS vars de `@arqel-dev/ui` (sem hardcode); `aria-invalid` quando há erros
29
+
30
+ **Por chegar (Phase 2):**
31
+
32
+ - Image crop (`react-image-crop`)
33
+ - Date-picker custom (`react-day-picker`)
34
+ - Combobox searchable de Base UI para `SelectInput`
35
+ - `SlugInput` auto-derivado de outro field (depende de FormContext exposed pelo `useArqelForm`)
36
+
37
+ ## Creating a custom field
38
+
39
+ ```tsx
40
+ // resources/js/fields/MoneyInput.tsx
41
+ import type { FieldRendererProps } from '@arqel-dev/ui/form';
42
+
43
+ export function MoneyInput({ field, value, onChange, errors, inputId }: FieldRendererProps) {
44
+ const hasError = errors !== undefined && errors.length > 0;
45
+ return (
46
+ <input
47
+ id={inputId}
48
+ type="text"
49
+ inputMode="decimal"
50
+ className="…"
51
+ value={typeof value === 'string' ? value : ''}
52
+ aria-invalid={hasError || undefined}
53
+ onChange={(e) => onChange(e.target.value)}
54
+ />
55
+ );
56
+ }
57
+ ```
58
+
59
+ ```tsx
60
+ // resources/js/app.tsx
61
+ import '@arqel-dev/ui/styles.css';
62
+ import '@arqel-dev/fields/register';
63
+ import { registerField } from '@arqel-dev/ui/form';
64
+ import { MoneyInput } from './fields/MoneyInput';
65
+
66
+ registerField('MoneyInput', MoneyInput); // depois do register.ts default
67
+ ```
68
+
69
+ ```php
70
+ // app/Arqel/Fields/MoneyField.php
71
+ final class MoneyField extends Field
72
+ {
73
+ protected string $type = 'money';
74
+ protected string $component = 'MoneyInput';
75
+ }
76
+ ```
77
+
78
+ ## Key Contracts
79
+
80
+ ```tsx
81
+ // resources/js/app.tsx
82
+ import '@arqel-dev/ui/styles.css';
83
+ import '@arqel-dev/fields/register'; // side effect: registra inputs ricos
84
+ import { createArqelApp } from '@arqel-dev/react/inertia';
85
+
86
+ createArqelApp({
87
+ appName: 'Acme Admin',
88
+ pages: import.meta.glob('./Pages/**/*.tsx'),
89
+ });
90
+ ```
91
+
92
+ A partir desse import, `<FieldRenderer>` resolve `field.component === 'TextInput'` para o componente rico. Sem o import, cai no fallback nativo de `@arqel-dev/ui/form` (`nativeFields.tsx`).
93
+
94
+ ```tsx
95
+ // Override custom: registre depois do side-effect import
96
+ import { registerField } from '@arqel-dev/ui/form';
97
+ import { MyFancyText } from './MyFancyText';
98
+
99
+ registerField('TextInput', MyFancyText);
100
+ ```
101
+
102
+ ## Conventions
103
+
104
+ - **Nome do componente** segue o `Field::component()` retornado pelo PHP
105
+ - **Props canônicas** vêm de `FieldRendererProps` (re-exportado de `@arqel-dev/ui/form`): `field`, `value`, `onChange`, `errors`, `disabled`, `inputId`, `describedBy`
106
+ - **Estilos** sempre via `@arqel-dev/ui/utils#cn` + CSS vars (`--color-arqel-*`) — nunca hardcode
107
+ - **A11y**: `aria-invalid` quando `errors.length > 0`, `aria-describedby` propagado, labels associados via `inputId` (gerenciado pelo `<FieldRenderer>`)
108
+ - **Side-effect entry**: `register.ts` é o único arquivo com `sideEffects: true` no `package.json`
109
+
110
+ ## Anti-patterns
111
+
112
+ - ❌ **Importar `register.ts` mais de uma vez** — registra 2x o mesmo componente; se você precisa override, chame `registerField` direto
113
+ - ❌ **Criar wrapper `<label>` interno** — `<FieldRenderer>` já faz isso; o componente recebe `inputId` para `<input id={inputId}>`
114
+ - ❌ **Hardcode de cor** — usa CSS vars
115
+ - ❌ **Importar de `@arqel-dev/fields/text/TextInput.js`** — usa subpaths declarados (`@arqel-dev/fields/text`)
116
+ - ❌ **Render label dentro do componente** — `<FieldRenderer>` é o dono do label
117
+
118
+ ## Related
119
+
120
+ - Tickets: [`PLANNING/08-fase-1-mvp.md`](../../PLANNING/08-fase-1-mvp.md) §FIELDS-JS-001..006
121
+ - API: [`PLANNING/06-api-react.md`](../../PLANNING/06-api-react.md) §10
122
+ - Source: [`packages-js/fields-js/src/`](src/)
123
+ - Tests: [`packages-js/fields-js/tests/`](tests/)
@@ -0,0 +1,8 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { FieldRendererProps } from '@arqel-dev/ui/form';
3
+
4
+ declare function Checkbox({ field, value, onChange, errors, disabled, inputId, describedBy, }: FieldRendererProps): react_jsx_runtime.JSX.Element;
5
+
6
+ declare function Toggle({ field, value, onChange, errors, disabled, inputId, describedBy, }: FieldRendererProps): react_jsx_runtime.JSX.Element;
7
+
8
+ export { Checkbox, Toggle };
@@ -0,0 +1,93 @@
1
+ import { cn } from '@arqel-dev/ui/utils';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+
4
+ // src/shared/styles.ts
5
+ cn(
6
+ "h-9 w-full rounded-sm border border-[var(--input)]",
7
+ "bg-background px-3 text-sm text-foreground",
8
+ "placeholder:text-muted-foreground",
9
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
10
+ "disabled:cursor-not-allowed disabled:opacity-50",
11
+ "aria-invalid:border-destructive"
12
+ );
13
+ var checkboxClasses = cn(
14
+ "h-4 w-4 rounded border-[var(--input)]",
15
+ "text-primary",
16
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
17
+ "disabled:cursor-not-allowed disabled:opacity-50"
18
+ );
19
+ function Checkbox({
20
+ field,
21
+ value,
22
+ onChange,
23
+ errors,
24
+ disabled,
25
+ inputId,
26
+ describedBy
27
+ }) {
28
+ const f = field;
29
+ const hasError = errors !== void 0 && errors.length > 0;
30
+ return /* @__PURE__ */ jsx(
31
+ "input",
32
+ {
33
+ id: inputId,
34
+ type: "checkbox",
35
+ className: checkboxClasses,
36
+ checked: value === true,
37
+ disabled: disabled || f.disabled || f.readonly,
38
+ "aria-invalid": hasError || void 0,
39
+ "aria-describedby": describedBy,
40
+ onChange: (e) => onChange(e.target.checked)
41
+ }
42
+ );
43
+ }
44
+ function Toggle({
45
+ field,
46
+ value,
47
+ onChange,
48
+ errors,
49
+ disabled,
50
+ inputId,
51
+ describedBy
52
+ }) {
53
+ const f = field;
54
+ const checked = value === true;
55
+ const hasError = errors !== void 0 && errors.length > 0;
56
+ const isDisabled = disabled || f.disabled || f.readonly;
57
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
58
+ /* @__PURE__ */ jsx(
59
+ "button",
60
+ {
61
+ id: inputId,
62
+ type: "button",
63
+ role: "switch",
64
+ "aria-checked": checked,
65
+ "aria-invalid": hasError || void 0,
66
+ "aria-describedby": describedBy,
67
+ disabled: isDisabled,
68
+ onClick: () => onChange(!checked),
69
+ className: cn(
70
+ "relative inline-flex h-6 w-11 items-center rounded-full transition-colors",
71
+ checked ? "bg-primary" : "bg-muted",
72
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
73
+ "disabled:cursor-not-allowed disabled:opacity-50"
74
+ ),
75
+ children: /* @__PURE__ */ jsx(
76
+ "span",
77
+ {
78
+ "aria-hidden": "true",
79
+ className: cn(
80
+ "inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform",
81
+ checked ? "translate-x-5" : "translate-x-0.5"
82
+ )
83
+ }
84
+ )
85
+ }
86
+ ),
87
+ (f.props.onLabel || f.props.offLabel) && /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: checked ? f.props.onLabel : f.props.offLabel })
88
+ ] });
89
+ }
90
+
91
+ export { Checkbox, Toggle };
92
+ //# sourceMappingURL=boolean.js.map
93
+ //# sourceMappingURL=boolean.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/styles.ts","../src/boolean/Checkbox.tsx","../src/boolean/Toggle.tsx"],"names":["jsx","cn"],"mappings":";;;;AAO4B,EAAA;AAAA,EAC1B,oDAAA;AAAA,EACA,4CAAA;AAAA,EACA,mCAAA;AAAA,EACA,yEAAA;AAAA,EACA,iDAAA;AAAA,EACA;AACF;AAEO,IAAM,eAAA,GAAkB,EAAA;AAAA,EAC7B,uCAAA;AAAA,EACA,cAAA;AAAA,EACA,yEAAA;AAAA,EACA;AACF,CAAA;ACjBO,SAAS,QAAA,CAAS;AAAA,EACvB,KAAA;AAAA,EACA,KAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAA,EAAuB;AACrB,EAAA,MAAM,CAAA,GAAI,KAAA;AACV,EAAA,MAAM,QAAA,GAAW,MAAA,KAAW,MAAA,IAAa,MAAA,CAAO,MAAA,GAAS,CAAA;AACzD,EAAA,uBACE,GAAA;AAAA,IAAC,OAAA;AAAA,IAAA;AAAA,MACC,EAAA,EAAI,OAAA;AAAA,MACJ,IAAA,EAAK,UAAA;AAAA,MACL,SAAA,EAAW,eAAA;AAAA,MACX,SAAS,KAAA,KAAU,IAAA;AAAA,MACnB,QAAA,EAAU,QAAA,IAAY,CAAA,CAAE,QAAA,IAAY,CAAA,CAAE,QAAA;AAAA,MACtC,gBAAc,QAAA,IAAY,MAAA;AAAA,MAC1B,kBAAA,EAAkB,WAAA;AAAA,MAClB,UAAU,CAAC,CAAA,KAAM,QAAA,CAAS,CAAA,CAAE,OAAO,OAAO;AAAA;AAAA,GAC5C;AAEJ;AChBO,SAAS,MAAA,CAAO;AAAA,EACrB,KAAA;AAAA,EACA,KAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAA,EAAuB;AACrB,EAAA,MAAM,CAAA,GAAI,KAAA;AACV,EAAA,MAAM,UAAU,KAAA,KAAU,IAAA;AAC1B,EAAA,MAAM,QAAA,GAAW,MAAA,KAAW,MAAA,IAAa,MAAA,CAAO,MAAA,GAAS,CAAA;AACzD,EAAA,MAAM,UAAA,GAAa,QAAA,IAAY,CAAA,CAAE,QAAA,IAAY,CAAA,CAAE,QAAA;AAE/C,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,yBAAA,EACb,QAAA,EAAA;AAAA,oBAAAA,GAAAA;AAAA,MAAC,QAAA;AAAA,MAAA;AAAA,QACC,EAAA,EAAI,OAAA;AAAA,QACJ,IAAA,EAAK,QAAA;AAAA,QACL,IAAA,EAAK,QAAA;AAAA,QACL,cAAA,EAAc,OAAA;AAAA,QACd,gBAAc,QAAA,IAAY,MAAA;AAAA,QAC1B,kBAAA,EAAkB,WAAA;AAAA,QAClB,QAAA,EAAU,UAAA;AAAA,QACV,OAAA,EAAS,MAAM,QAAA,CAAS,CAAC,OAAO,CAAA;AAAA,QAChC,SAAA,EAAWC,EAAAA;AAAA,UACT,2EAAA;AAAA,UACA,UAAU,YAAA,GAAe,UAAA;AAAA,UACzB,yEAAA;AAAA,UACA;AAAA,SACF;AAAA,QAEA,QAAA,kBAAAD,GAAAA;AAAA,UAAC,MAAA;AAAA,UAAA;AAAA,YACC,aAAA,EAAY,MAAA;AAAA,YACZ,SAAA,EAAWC,EAAAA;AAAA,cACT,kFAAA;AAAA,cACA,UAAU,eAAA,GAAkB;AAAA;AAC9B;AAAA;AACF;AAAA,KACF;AAAA,IAAA,CACE,EAAE,KAAA,CAAM,OAAA,IAAW,CAAA,CAAE,KAAA,CAAM,6BAC3BD,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,iCACb,QAAA,EAAA,OAAA,GAAU,CAAA,CAAE,MAAM,OAAA,GAAU,CAAA,CAAE,MAAM,QAAA,EACvC;AAAA,GAAA,EAEJ,CAAA;AAEJ","file":"boolean.js","sourcesContent":["/**\n * Shared input styling — keeps the visual contract consistent across\n * every Field component without forcing a wrapper component.\n */\n\nimport { cn } from '@arqel-dev/ui/utils';\n\nexport const inputClasses = cn(\n 'h-9 w-full rounded-sm border border-[var(--input)]',\n 'bg-background px-3 text-sm text-foreground',\n 'placeholder:text-muted-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n 'aria-invalid:border-destructive',\n);\n\nexport const checkboxClasses = cn(\n 'h-4 w-4 rounded border-[var(--input)]',\n 'text-primary',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n);\n","import type { BooleanFieldSchema } from '@arqel-dev/types/fields';\nimport type { FieldRendererProps } from '@arqel-dev/ui/form';\nimport { checkboxClasses } from '../shared/styles.js';\n\nexport function Checkbox({\n field,\n value,\n onChange,\n errors,\n disabled,\n inputId,\n describedBy,\n}: FieldRendererProps) {\n const f = field as BooleanFieldSchema;\n const hasError = errors !== undefined && errors.length > 0;\n return (\n <input\n id={inputId}\n type=\"checkbox\"\n className={checkboxClasses}\n checked={value === true}\n disabled={disabled || f.disabled || f.readonly}\n aria-invalid={hasError || undefined}\n aria-describedby={describedBy}\n onChange={(e) => onChange(e.target.checked)}\n />\n );\n}\n","/**\n * `<Toggle>` — switch-style boolean control with optional on/off labels.\n *\n * Renders as a `role=\"switch\"` button so screen readers announce it\n * correctly; styling is the canonical iOS-style track/thumb.\n */\n\nimport type { ToggleFieldSchema } from '@arqel-dev/types/fields';\nimport type { FieldRendererProps } from '@arqel-dev/ui/form';\nimport { cn } from '@arqel-dev/ui/utils';\n\nexport function Toggle({\n field,\n value,\n onChange,\n errors,\n disabled,\n inputId,\n describedBy,\n}: FieldRendererProps) {\n const f = field as ToggleFieldSchema;\n const checked = value === true;\n const hasError = errors !== undefined && errors.length > 0;\n const isDisabled = disabled || f.disabled || f.readonly;\n\n return (\n <div className=\"flex items-center gap-3\">\n <button\n id={inputId}\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-invalid={hasError || undefined}\n aria-describedby={describedBy}\n disabled={isDisabled}\n onClick={() => onChange(!checked)}\n className={cn(\n 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',\n checked ? 'bg-primary' : 'bg-muted',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n )}\n >\n <span\n aria-hidden=\"true\"\n className={cn(\n 'inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform',\n checked ? 'translate-x-5' : 'translate-x-0.5',\n )}\n />\n </button>\n {(f.props.onLabel || f.props.offLabel) && (\n <span className=\"text-sm text-muted-foreground\">\n {checked ? f.props.onLabel : f.props.offLabel}\n </span>\n )}\n </div>\n );\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { FieldRendererProps } from '@arqel-dev/ui/form';
3
+
4
+ declare function ColorInput({ field, value, onChange, errors, disabled, inputId, describedBy, }: FieldRendererProps): react_jsx_runtime.JSX.Element;
5
+
6
+ export { ColorInput };
package/dist/color.js ADDED
@@ -0,0 +1,63 @@
1
+ import { cn } from '@arqel-dev/ui/utils';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+
4
+ // src/color/ColorInput.tsx
5
+ function ColorInput({
6
+ field,
7
+ value,
8
+ onChange,
9
+ errors,
10
+ disabled,
11
+ inputId,
12
+ describedBy
13
+ }) {
14
+ const f = field;
15
+ const hasError = errors !== void 0 && errors.length > 0;
16
+ const isDisabled = disabled || f.disabled || f.readonly;
17
+ const current = typeof value === "string" && value.length > 0 ? value : "#000000";
18
+ const presets = f.props.presets ?? [];
19
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
20
+ /* @__PURE__ */ jsx(
21
+ "input",
22
+ {
23
+ id: inputId,
24
+ type: "color",
25
+ className: cn(
26
+ "h-9 w-12 cursor-pointer rounded-sm border border-[var(--input)]",
27
+ "disabled:cursor-not-allowed disabled:opacity-50"
28
+ ),
29
+ value: current,
30
+ disabled: isDisabled,
31
+ "aria-invalid": hasError || void 0,
32
+ "aria-describedby": describedBy,
33
+ onChange: (e) => onChange(e.target.value)
34
+ }
35
+ ),
36
+ /* @__PURE__ */ jsx(
37
+ "input",
38
+ {
39
+ type: "text",
40
+ className: "h-9 w-28 rounded-sm border border-[var(--input)] bg-background px-2 font-mono text-xs",
41
+ value: current,
42
+ disabled: isDisabled,
43
+ onChange: (e) => onChange(e.target.value)
44
+ }
45
+ ),
46
+ presets.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: presets.map((preset) => /* @__PURE__ */ jsx(
47
+ "button",
48
+ {
49
+ type: "button",
50
+ "aria-label": `Preset ${preset}`,
51
+ disabled: isDisabled,
52
+ onClick: () => onChange(preset),
53
+ className: "h-6 w-6 rounded-full border border-border disabled:cursor-not-allowed disabled:opacity-50",
54
+ style: { backgroundColor: preset }
55
+ },
56
+ preset
57
+ )) })
58
+ ] });
59
+ }
60
+
61
+ export { ColorInput };
62
+ //# sourceMappingURL=color.js.map
63
+ //# sourceMappingURL=color.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/color/ColorInput.tsx"],"names":[],"mappings":";;;;AAYO,SAAS,UAAA,CAAW;AAAA,EACzB,KAAA;AAAA,EACA,KAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAA,EAAuB;AACrB,EAAA,MAAM,CAAA,GAAI,KAAA;AACV,EAAA,MAAM,QAAA,GAAW,MAAA,KAAW,MAAA,IAAa,MAAA,CAAO,MAAA,GAAS,CAAA;AACzD,EAAA,MAAM,UAAA,GAAa,QAAA,IAAY,CAAA,CAAE,QAAA,IAAY,CAAA,CAAE,QAAA;AAC/C,EAAA,MAAM,UAAU,OAAO,KAAA,KAAU,YAAY,KAAA,CAAM,MAAA,GAAS,IAAI,KAAA,GAAQ,SAAA;AACxE,EAAA,MAAM,OAAA,GAAU,CAAA,CAAE,KAAA,CAAM,OAAA,IAAW,EAAC;AAEpC,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,mCAAA,EACb,QAAA,EAAA;AAAA,oBAAA,GAAA;AAAA,MAAC,OAAA;AAAA,MAAA;AAAA,QACC,EAAA,EAAI,OAAA;AAAA,QACJ,IAAA,EAAK,OAAA;AAAA,QACL,SAAA,EAAW,EAAA;AAAA,UACT,iEAAA;AAAA,UACA;AAAA,SACF;AAAA,QACA,KAAA,EAAO,OAAA;AAAA,QACP,QAAA,EAAU,UAAA;AAAA,QACV,gBAAc,QAAA,IAAY,MAAA;AAAA,QAC1B,kBAAA,EAAkB,WAAA;AAAA,QAClB,UAAU,CAAC,CAAA,KAAM,QAAA,CAAS,CAAA,CAAE,OAAO,KAAK;AAAA;AAAA,KAC1C;AAAA,oBACA,GAAA;AAAA,MAAC,OAAA;AAAA,MAAA;AAAA,QACC,IAAA,EAAK,MAAA;AAAA,QACL,SAAA,EAAU,uFAAA;AAAA,QACV,KAAA,EAAO,OAAA;AAAA,QACP,QAAA,EAAU,UAAA;AAAA,QACV,UAAU,CAAC,CAAA,KAAM,QAAA,CAAS,CAAA,CAAE,OAAO,KAAK;AAAA;AAAA,KAC1C;AAAA,IACC,OAAA,CAAQ,MAAA,GAAS,CAAA,oBAChB,GAAA,CAAC,KAAA,EAAA,EAAI,WAAU,sBAAA,EACZ,QAAA,EAAA,OAAA,CAAQ,GAAA,CAAI,CAAC,MAAA,qBACZ,GAAA;AAAA,MAAC,QAAA;AAAA,MAAA;AAAA,QAEC,IAAA,EAAK,QAAA;AAAA,QACL,YAAA,EAAY,UAAU,MAAM,CAAA,CAAA;AAAA,QAC5B,QAAA,EAAU,UAAA;AAAA,QACV,OAAA,EAAS,MAAM,QAAA,CAAS,MAAM,CAAA;AAAA,QAC9B,SAAA,EAAU,2FAAA;AAAA,QACV,KAAA,EAAO,EAAE,eAAA,EAAiB,MAAA;AAAO,OAAA;AAAA,MAN5B;AAAA,KAQR,CAAA,EACH;AAAA,GAAA,EAEJ,CAAA;AAEJ","file":"color.js","sourcesContent":["/**\n * `<ColorInput>` — native color picker plus optional preset swatches.\n *\n * `field.props.presets: string[]` (hex strings) renders a strip of\n * clickable swatches above the picker. The native `<input type=\"color\">`\n * is a/11y-friendly out of the box.\n */\n\nimport type { ColorFieldSchema } from '@arqel-dev/types/fields';\nimport type { FieldRendererProps } from '@arqel-dev/ui/form';\nimport { cn } from '@arqel-dev/ui/utils';\n\nexport function ColorInput({\n field,\n value,\n onChange,\n errors,\n disabled,\n inputId,\n describedBy,\n}: FieldRendererProps) {\n const f = field as ColorFieldSchema;\n const hasError = errors !== undefined && errors.length > 0;\n const isDisabled = disabled || f.disabled || f.readonly;\n const current = typeof value === 'string' && value.length > 0 ? value : '#000000';\n const presets = f.props.presets ?? [];\n\n return (\n <div className=\"flex flex-wrap items-center gap-2\">\n <input\n id={inputId}\n type=\"color\"\n className={cn(\n 'h-9 w-12 cursor-pointer rounded-sm border border-[var(--input)]',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n )}\n value={current}\n disabled={isDisabled}\n aria-invalid={hasError || undefined}\n aria-describedby={describedBy}\n onChange={(e) => onChange(e.target.value)}\n />\n <input\n type=\"text\"\n className=\"h-9 w-28 rounded-sm border border-[var(--input)] bg-background px-2 font-mono text-xs\"\n value={current}\n disabled={isDisabled}\n onChange={(e) => onChange(e.target.value)}\n />\n {presets.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {presets.map((preset) => (\n <button\n key={preset}\n type=\"button\"\n aria-label={`Preset ${preset}`}\n disabled={isDisabled}\n onClick={() => onChange(preset)}\n className=\"h-6 w-6 rounded-full border border-border disabled:cursor-not-allowed disabled:opacity-50\"\n style={{ backgroundColor: preset }}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n"]}
package/dist/date.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { FieldRendererProps } from '@arqel-dev/ui/form';
3
+
4
+ declare function DateInput({ field, value, onChange, errors, disabled, inputId, describedBy, }: FieldRendererProps): react_jsx_runtime.JSX.Element;
5
+
6
+ declare function DateTimeInput({ field, value, onChange, errors, disabled, inputId, describedBy, }: FieldRendererProps): react_jsx_runtime.JSX.Element;
7
+
8
+ export { DateInput, DateTimeInput };
package/dist/date.js ADDED
@@ -0,0 +1,81 @@
1
+ import { cn } from '@arqel-dev/ui/utils';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ // src/shared/styles.ts
5
+ var inputClasses = cn(
6
+ "h-9 w-full rounded-sm border border-[var(--input)]",
7
+ "bg-background px-3 text-sm text-foreground",
8
+ "placeholder:text-muted-foreground",
9
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
10
+ "disabled:cursor-not-allowed disabled:opacity-50",
11
+ "aria-invalid:border-destructive"
12
+ );
13
+ cn(
14
+ "h-4 w-4 rounded border-[var(--input)]",
15
+ "text-primary",
16
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
17
+ "disabled:cursor-not-allowed disabled:opacity-50"
18
+ );
19
+ function DateInput({
20
+ field,
21
+ value,
22
+ onChange,
23
+ errors,
24
+ disabled,
25
+ inputId,
26
+ describedBy
27
+ }) {
28
+ const f = field;
29
+ const hasError = errors !== void 0 && errors.length > 0;
30
+ return /* @__PURE__ */ jsx(
31
+ "input",
32
+ {
33
+ id: inputId,
34
+ type: "date",
35
+ className: inputClasses,
36
+ value: typeof value === "string" ? value : "",
37
+ disabled: disabled || f.disabled || f.readonly,
38
+ readOnly: f.readonly === true,
39
+ required: f.required === true,
40
+ min: f.props.minDate,
41
+ max: f.props.maxDate,
42
+ "aria-invalid": hasError || void 0,
43
+ "aria-describedby": describedBy,
44
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
45
+ }
46
+ );
47
+ }
48
+ function DateTimeInput({
49
+ field,
50
+ value,
51
+ onChange,
52
+ errors,
53
+ disabled,
54
+ inputId,
55
+ describedBy
56
+ }) {
57
+ const f = field;
58
+ const hasError = errors !== void 0 && errors.length > 0;
59
+ return /* @__PURE__ */ jsx(
60
+ "input",
61
+ {
62
+ id: inputId,
63
+ type: "datetime-local",
64
+ className: inputClasses,
65
+ value: typeof value === "string" ? value : "",
66
+ disabled: disabled || f.disabled || f.readonly,
67
+ readOnly: f.readonly === true,
68
+ required: f.required === true,
69
+ min: f.props.minDate,
70
+ max: f.props.maxDate,
71
+ step: f.props.seconds ? 1 : void 0,
72
+ "aria-invalid": hasError || void 0,
73
+ "aria-describedby": describedBy,
74
+ onChange: (e) => onChange(e.target.value === "" ? null : e.target.value)
75
+ }
76
+ );
77
+ }
78
+
79
+ export { DateInput, DateTimeInput };
80
+ //# sourceMappingURL=date.js.map
81
+ //# sourceMappingURL=date.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shared/styles.ts","../src/date/DateInput.tsx","../src/date/DateTimeInput.tsx"],"names":["jsx"],"mappings":";;;;AAOO,IAAM,YAAA,GAAe,EAAA;AAAA,EAC1B,oDAAA;AAAA,EACA,4CAAA;AAAA,EACA,mCAAA;AAAA,EACA,yEAAA;AAAA,EACA,iDAAA;AAAA,EACA;AACF,CAAA;AAE+B,EAAA;AAAA,EAC7B,uCAAA;AAAA,EACA,cAAA;AAAA,EACA,yEAAA;AAAA,EACA;AACF;ACRO,SAAS,SAAA,CAAU;AAAA,EACxB,KAAA;AAAA,EACA,KAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAA,EAAuB;AACrB,EAAA,MAAM,CAAA,GAAI,KAAA;AACV,EAAA,MAAM,QAAA,GAAW,MAAA,KAAW,MAAA,IAAa,MAAA,CAAO,MAAA,GAAS,CAAA;AACzD,EAAA,uBACE,GAAA;AAAA,IAAC,OAAA;AAAA,IAAA;AAAA,MACC,EAAA,EAAI,OAAA;AAAA,MACJ,IAAA,EAAK,MAAA;AAAA,MACL,SAAA,EAAW,YAAA;AAAA,MACX,KAAA,EAAO,OAAO,KAAA,KAAU,QAAA,GAAW,KAAA,GAAQ,EAAA;AAAA,MAC3C,QAAA,EAAU,QAAA,IAAY,CAAA,CAAE,QAAA,IAAY,CAAA,CAAE,QAAA;AAAA,MACtC,QAAA,EAAU,EAAE,QAAA,KAAa,IAAA;AAAA,MACzB,QAAA,EAAU,EAAE,QAAA,KAAa,IAAA;AAAA,MACzB,GAAA,EAAK,EAAE,KAAA,CAAM,OAAA;AAAA,MACb,GAAA,EAAK,EAAE,KAAA,CAAM,OAAA;AAAA,MACb,gBAAc,QAAA,IAAY,MAAA;AAAA,MAC1B,kBAAA,EAAkB,WAAA;AAAA,MAClB,QAAA,EAAU,CAAC,CAAA,KAAM,QAAA,CAAS,CAAA,CAAE,MAAA,CAAO,KAAA,KAAU,EAAA,GAAK,IAAA,GAAO,CAAA,CAAE,MAAA,CAAO,KAAK;AAAA;AAAA,GACzE;AAEJ;ACpCO,SAAS,aAAA,CAAc;AAAA,EAC5B,KAAA;AAAA,EACA,KAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAA,EAAuB;AACrB,EAAA,MAAM,CAAA,GAAI,KAAA;AACV,EAAA,MAAM,QAAA,GAAW,MAAA,KAAW,MAAA,IAAa,MAAA,CAAO,MAAA,GAAS,CAAA;AACzD,EAAA,uBACEA,GAAAA;AAAA,IAAC,OAAA;AAAA,IAAA;AAAA,MACC,EAAA,EAAI,OAAA;AAAA,MACJ,IAAA,EAAK,gBAAA;AAAA,MACL,SAAA,EAAW,YAAA;AAAA,MACX,KAAA,EAAO,OAAO,KAAA,KAAU,QAAA,GAAW,KAAA,GAAQ,EAAA;AAAA,MAC3C,QAAA,EAAU,QAAA,IAAY,CAAA,CAAE,QAAA,IAAY,CAAA,CAAE,QAAA;AAAA,MACtC,QAAA,EAAU,EAAE,QAAA,KAAa,IAAA;AAAA,MACzB,QAAA,EAAU,EAAE,QAAA,KAAa,IAAA;AAAA,MACzB,GAAA,EAAK,EAAE,KAAA,CAAM,OAAA;AAAA,MACb,GAAA,EAAK,EAAE,KAAA,CAAM,OAAA;AAAA,MACb,IAAA,EAAM,CAAA,CAAE,KAAA,CAAM,OAAA,GAAU,CAAA,GAAI,MAAA;AAAA,MAC5B,gBAAc,QAAA,IAAY,MAAA;AAAA,MAC1B,kBAAA,EAAkB,WAAA;AAAA,MAClB,QAAA,EAAU,CAAC,CAAA,KAAM,QAAA,CAAS,CAAA,CAAE,MAAA,CAAO,KAAA,KAAU,EAAA,GAAK,IAAA,GAAO,CAAA,CAAE,MAAA,CAAO,KAAK;AAAA;AAAA,GACzE;AAEJ","file":"date.js","sourcesContent":["/**\n * Shared input styling — keeps the visual contract consistent across\n * every Field component without forcing a wrapper component.\n */\n\nimport { cn } from '@arqel-dev/ui/utils';\n\nexport const inputClasses = cn(\n 'h-9 w-full rounded-sm border border-[var(--input)]',\n 'bg-background px-3 text-sm text-foreground',\n 'placeholder:text-muted-foreground',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n 'aria-invalid:border-destructive',\n);\n\nexport const checkboxClasses = cn(\n 'h-4 w-4 rounded border-[var(--input)]',\n 'text-primary',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n);\n","/**\n * `<DateInput>` — native `<input type=\"date\">`.\n *\n * Phase 1 leans on the browser's built-in date picker for footprint\n * reasons. A `react-day-picker`-based custom picker lands in Phase 2\n * when we need locale-specific formatting + min/max enforcement\n * beyond what the native control provides.\n */\n\nimport type { DateFieldSchema } from '@arqel-dev/types/fields';\nimport type { FieldRendererProps } from '@arqel-dev/ui/form';\nimport { inputClasses } from '../shared/styles.js';\n\nexport function DateInput({\n field,\n value,\n onChange,\n errors,\n disabled,\n inputId,\n describedBy,\n}: FieldRendererProps) {\n const f = field as DateFieldSchema;\n const hasError = errors !== undefined && errors.length > 0;\n return (\n <input\n id={inputId}\n type=\"date\"\n className={inputClasses}\n value={typeof value === 'string' ? value : ''}\n disabled={disabled || f.disabled || f.readonly}\n readOnly={f.readonly === true}\n required={f.required === true}\n min={f.props.minDate}\n max={f.props.maxDate}\n aria-invalid={hasError || undefined}\n aria-describedby={describedBy}\n onChange={(e) => onChange(e.target.value === '' ? null : e.target.value)}\n />\n );\n}\n","import type { DateTimeFieldSchema } from '@arqel-dev/types/fields';\nimport type { FieldRendererProps } from '@arqel-dev/ui/form';\nimport { inputClasses } from '../shared/styles.js';\n\nexport function DateTimeInput({\n field,\n value,\n onChange,\n errors,\n disabled,\n inputId,\n describedBy,\n}: FieldRendererProps) {\n const f = field as DateTimeFieldSchema;\n const hasError = errors !== undefined && errors.length > 0;\n return (\n <input\n id={inputId}\n type=\"datetime-local\"\n className={inputClasses}\n value={typeof value === 'string' ? value : ''}\n disabled={disabled || f.disabled || f.readonly}\n readOnly={f.readonly === true}\n required={f.required === true}\n min={f.props.minDate}\n max={f.props.maxDate}\n step={f.props.seconds ? 1 : undefined}\n aria-invalid={hasError || undefined}\n aria-describedby={describedBy}\n onChange={(e) => onChange(e.target.value === '' ? null : e.target.value)}\n />\n );\n}\n"]}
package/dist/file.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { FieldRendererProps } from '@arqel-dev/ui/form';
3
+
4
+ declare function FileInput({ field, value, onChange, errors, disabled, inputId, describedBy, }: FieldRendererProps): react_jsx_runtime.JSX.Element;
5
+
6
+ declare function ImageInput({ field, value, onChange, errors, disabled, inputId, describedBy, }: FieldRendererProps): react_jsx_runtime.JSX.Element;
7
+
8
+ export { FileInput, ImageInput };
package/dist/file.js ADDED
@@ -0,0 +1,135 @@
1
+ import { cn } from '@arqel-dev/ui/utils';
2
+ import { useState, useEffect } from 'react';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+
5
+ // src/file/FileInput.tsx
6
+ function FileInput({
7
+ field,
8
+ value,
9
+ onChange,
10
+ errors,
11
+ disabled,
12
+ inputId,
13
+ describedBy
14
+ }) {
15
+ const f = field;
16
+ const hasError = errors !== void 0 && errors.length > 0;
17
+ const isDisabled = disabled || f.disabled || f.readonly;
18
+ const [dragOver, setDragOver] = useState(false);
19
+ const file = value instanceof File ? value : null;
20
+ const filename = file?.name ?? (typeof value === "string" ? value : null);
21
+ const handleFiles = (files) => {
22
+ if (!files || files.length === 0) return;
23
+ onChange(files[0]);
24
+ };
25
+ return /* @__PURE__ */ jsxs(
26
+ "section",
27
+ {
28
+ "aria-label": "File upload",
29
+ onDragOver: (e) => {
30
+ e.preventDefault();
31
+ if (!isDisabled) setDragOver(true);
32
+ },
33
+ onDragLeave: () => setDragOver(false),
34
+ onDrop: (e) => {
35
+ e.preventDefault();
36
+ setDragOver(false);
37
+ if (!isDisabled) handleFiles(e.dataTransfer.files);
38
+ },
39
+ className: cn(
40
+ "flex flex-col items-center justify-center gap-2 rounded-sm border-2 border-dashed px-4 py-6 text-sm",
41
+ dragOver ? "border-primary bg-muted" : "border-border",
42
+ hasError && "border-destructive",
43
+ isDisabled && "opacity-50"
44
+ ),
45
+ children: [
46
+ 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" }),
47
+ /* @__PURE__ */ jsxs("label", { className: "cursor-pointer text-xs text-primary hover:underline", children: [
48
+ filename ? "Choose another file" : "Browse",
49
+ /* @__PURE__ */ jsx(
50
+ "input",
51
+ {
52
+ id: inputId,
53
+ type: "file",
54
+ className: "sr-only",
55
+ disabled: isDisabled,
56
+ "aria-invalid": hasError || void 0,
57
+ "aria-describedby": describedBy,
58
+ onChange: (e) => handleFiles(e.target.files)
59
+ }
60
+ )
61
+ ] })
62
+ ]
63
+ }
64
+ );
65
+ }
66
+ function ImageInput({
67
+ field,
68
+ value,
69
+ onChange,
70
+ errors,
71
+ disabled,
72
+ inputId,
73
+ describedBy
74
+ }) {
75
+ const f = field;
76
+ const hasError = errors !== void 0 && errors.length > 0;
77
+ const isDisabled = disabled || f.disabled || f.readonly;
78
+ const [previewUrl, setPreviewUrl] = useState(null);
79
+ useEffect(() => {
80
+ if (value instanceof File) {
81
+ const url = URL.createObjectURL(value);
82
+ setPreviewUrl(url);
83
+ return () => URL.revokeObjectURL(url);
84
+ }
85
+ if (typeof value === "string" && value.length > 0) {
86
+ setPreviewUrl(value);
87
+ return void 0;
88
+ }
89
+ setPreviewUrl(null);
90
+ return void 0;
91
+ }, [value]);
92
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
93
+ previewUrl && /* @__PURE__ */ jsx(
94
+ "img",
95
+ {
96
+ src: previewUrl,
97
+ alt: "Preview",
98
+ className: cn("max-h-40 w-auto rounded-sm border border-border")
99
+ }
100
+ ),
101
+ /* @__PURE__ */ jsxs(
102
+ "label",
103
+ {
104
+ className: cn(
105
+ "inline-flex w-fit cursor-pointer items-center gap-2 rounded-sm border border-border px-3 py-1.5 text-sm",
106
+ "hover:bg-muted",
107
+ isDisabled && "cursor-not-allowed opacity-50"
108
+ ),
109
+ children: [
110
+ previewUrl ? "Replace image" : "Choose image",
111
+ /* @__PURE__ */ jsx(
112
+ "input",
113
+ {
114
+ id: inputId,
115
+ type: "file",
116
+ accept: "image/*",
117
+ className: "sr-only",
118
+ disabled: isDisabled,
119
+ "aria-invalid": hasError || void 0,
120
+ "aria-describedby": describedBy,
121
+ onChange: (e) => {
122
+ const file = e.target.files?.[0];
123
+ if (file) onChange(file);
124
+ }
125
+ }
126
+ )
127
+ ]
128
+ }
129
+ )
130
+ ] });
131
+ }
132
+
133
+ export { FileInput, ImageInput };
134
+ //# sourceMappingURL=file.js.map
135
+ //# sourceMappingURL=file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/file/FileInput.tsx","../src/file/ImageInput.tsx"],"names":["useState","jsxs","jsx","cn"],"mappings":";;;;;AAcO,SAAS,SAAA,CAAU;AAAA,EACxB,KAAA;AAAA,EACA,KAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAA,EAAuB;AACrB,EAAA,MAAM,CAAA,GAAI,KAAA;AACV,EAAA,MAAM,QAAA,GAAW,MAAA,KAAW,MAAA,IAAa,MAAA,CAAO,MAAA,GAAS,CAAA;AACzD,EAAA,MAAM,UAAA,GAAa,QAAA,IAAY,CAAA,CAAE,QAAA,IAAY,CAAA,CAAE,QAAA;AAC/C,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAI,SAAS,KAAK,CAAA;AAE9C,EAAA,MAAM,IAAA,GAAO,KAAA,YAAiB,IAAA,GAAO,KAAA,GAAQ,IAAA;AAC7C,EAAA,MAAM,WAAW,IAAA,EAAM,IAAA,KAAS,OAAO,KAAA,KAAU,WAAW,KAAA,GAAQ,IAAA,CAAA;AAEpE,EAAA,MAAM,WAAA,GAAc,CAAC,KAAA,KAA2B;AAC9C,IAAA,IAAI,CAAC,KAAA,IAAS,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG;AAClC,IAAA,QAAA,CAAS,KAAA,CAAM,CAAC,CAAC,CAAA;AAAA,EACnB,CAAA;AAEA,EAAA,uBACE,IAAA;AAAA,IAAC,SAAA;AAAA,IAAA;AAAA,MACC,YAAA,EAAW,aAAA;AAAA,MACX,UAAA,EAAY,CAAC,CAAA,KAAM;AACjB,QAAA,CAAA,CAAE,cAAA,EAAe;AACjB,QAAA,IAAI,CAAC,UAAA,EAAY,WAAA,CAAY,IAAI,CAAA;AAAA,MACnC,CAAA;AAAA,MACA,WAAA,EAAa,MAAM,WAAA,CAAY,KAAK,CAAA;AAAA,MACpC,MAAA,EAAQ,CAAC,CAAA,KAAM;AACb,QAAA,CAAA,CAAE,cAAA,EAAe;AACjB,QAAA,WAAA,CAAY,KAAK,CAAA;AACjB,QAAA,IAAI,CAAC,UAAA,EAAY,WAAA,CAAY,CAAA,CAAE,aAAa,KAAK,CAAA;AAAA,MACnD,CAAA;AAAA,MACA,SAAA,EAAW,EAAA;AAAA,QACT,qGAAA;AAAA,QACA,WAAW,yBAAA,GAA4B,eAAA;AAAA,QACvC,QAAA,IAAY,oBAAA;AAAA,QACZ,UAAA,IAAc;AAAA,OAChB;AAAA,MAEC,QAAA,EAAA;AAAA,QAAA,QAAA,mBACC,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,aAAA,EAAe,QAAA,EAAA,QAAA,EAAS,oBAExC,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,uBAAA,EAAwB,QAAA,EAAA,qCAAA,EAAmC,CAAA;AAAA,wBAE7E,IAAA,CAAC,OAAA,EAAA,EAAM,SAAA,EAAU,qDAAA,EACd,QAAA,EAAA;AAAA,UAAA,QAAA,GAAW,qBAAA,GAAwB,QAAA;AAAA,0BACpC,GAAA;AAAA,YAAC,OAAA;AAAA,YAAA;AAAA,cACC,EAAA,EAAI,OAAA;AAAA,cACJ,IAAA,EAAK,MAAA;AAAA,cACL,SAAA,EAAU,SAAA;AAAA,cACV,QAAA,EAAU,UAAA;AAAA,cACV,gBAAc,QAAA,IAAY,MAAA;AAAA,cAC1B,kBAAA,EAAkB,WAAA;AAAA,cAClB,UAAU,CAAC,CAAA,KAAM,WAAA,CAAY,CAAA,CAAE,OAAO,KAAK;AAAA;AAAA;AAC7C,SAAA,EACF;AAAA;AAAA;AAAA,GACF;AAEJ;AC/DO,SAAS,UAAA,CAAW;AAAA,EACzB,KAAA;AAAA,EACA,KAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAA,EAAuB;AACrB,EAAA,MAAM,CAAA,GAAI,KAAA;AACV,EAAA,MAAM,QAAA,GAAW,MAAA,KAAW,MAAA,IAAa,MAAA,CAAO,MAAA,GAAS,CAAA;AACzD,EAAA,MAAM,UAAA,GAAa,QAAA,IAAY,CAAA,CAAE,QAAA,IAAY,CAAA,CAAE,QAAA;AAE/C,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAIA,SAAwB,IAAI,CAAA;AAEhE,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,iBAAiB,IAAA,EAAM;AACzB,MAAA,MAAM,GAAA,GAAM,GAAA,CAAI,eAAA,CAAgB,KAAK,CAAA;AACrC,MAAA,aAAA,CAAc,GAAG,CAAA;AACjB,MAAA,OAAO,MAAM,GAAA,CAAI,eAAA,CAAgB,GAAG,CAAA;AAAA,IACtC;AACA,IAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,CAAM,SAAS,CAAA,EAAG;AACjD,MAAA,aAAA,CAAc,KAAK,CAAA;AACnB,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,aAAA,CAAc,IAAI,CAAA;AAClB,IAAA,OAAO,MAAA;AAAA,EACT,CAAA,EAAG,CAAC,KAAK,CAAC,CAAA;AAEV,EAAA,uBACEC,IAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,qBAAA,EACZ,QAAA,EAAA;AAAA,IAAA,UAAA,oBACCC,GAAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,GAAA,EAAK,UAAA;AAAA,QACL,GAAA,EAAI,SAAA;AAAA,QACJ,SAAA,EAAWC,GAAG,iDAAiD;AAAA;AAAA,KACjE;AAAA,oBAEFF,IAAAA;AAAA,MAAC,OAAA;AAAA,MAAA;AAAA,QACC,SAAA,EAAWE,EAAAA;AAAA,UACT,yGAAA;AAAA,UACA,gBAAA;AAAA,UACA,UAAA,IAAc;AAAA,SAChB;AAAA,QAEC,QAAA,EAAA;AAAA,UAAA,UAAA,GAAa,eAAA,GAAkB,cAAA;AAAA,0BAChCD,GAAAA;AAAA,YAAC,OAAA;AAAA,YAAA;AAAA,cACC,EAAA,EAAI,OAAA;AAAA,cACJ,IAAA,EAAK,MAAA;AAAA,cACL,MAAA,EAAO,SAAA;AAAA,cACP,SAAA,EAAU,SAAA;AAAA,cACV,QAAA,EAAU,UAAA;AAAA,cACV,gBAAc,QAAA,IAAY,MAAA;AAAA,cAC1B,kBAAA,EAAkB,WAAA;AAAA,cAClB,QAAA,EAAU,CAAC,CAAA,KAAM;AACf,gBAAA,MAAM,IAAA,GAAO,CAAA,CAAE,MAAA,CAAO,KAAA,GAAQ,CAAC,CAAA;AAC/B,gBAAA,IAAI,IAAA,WAAe,IAAI,CAAA;AAAA,cACzB;AAAA;AAAA;AACF;AAAA;AAAA;AACF,GAAA,EACF,CAAA;AAEJ","file":"file.js","sourcesContent":["/**\n * `<FileInput>` — drag-drop file picker.\n *\n * Phase 1 stores the selected `File` object directly in the form\n * state; the actual upload happens server-side via Inertia's\n * multipart-aware form submission. Progress events are exposed\n * via `useArqelForm`'s `progress` field (Inertia native).\n */\n\nimport type { FileFieldSchema } from '@arqel-dev/types/fields';\nimport type { FieldRendererProps } from '@arqel-dev/ui/form';\nimport { cn } from '@arqel-dev/ui/utils';\nimport { useState } from 'react';\n\nexport function FileInput({\n field,\n value,\n onChange,\n errors,\n disabled,\n inputId,\n describedBy,\n}: FieldRendererProps) {\n const f = field as FileFieldSchema;\n const hasError = errors !== undefined && errors.length > 0;\n const isDisabled = disabled || f.disabled || f.readonly;\n const [dragOver, setDragOver] = useState(false);\n\n const file = value instanceof File ? value : null;\n const filename = file?.name ?? (typeof value === 'string' ? value : null);\n\n const handleFiles = (files: FileList | null) => {\n if (!files || files.length === 0) return;\n onChange(files[0]);\n };\n\n return (\n <section\n aria-label=\"File upload\"\n onDragOver={(e) => {\n e.preventDefault();\n if (!isDisabled) setDragOver(true);\n }}\n onDragLeave={() => setDragOver(false)}\n onDrop={(e) => {\n e.preventDefault();\n setDragOver(false);\n if (!isDisabled) handleFiles(e.dataTransfer.files);\n }}\n className={cn(\n 'flex flex-col items-center justify-center gap-2 rounded-sm border-2 border-dashed px-4 py-6 text-sm',\n dragOver ? 'border-primary bg-muted' : 'border-border',\n hasError && 'border-destructive',\n isDisabled && 'opacity-50',\n )}\n >\n {filename ? (\n <span className=\"font-medium\">{filename}</span>\n ) : (\n <span className=\"text-muted-foreground\">Drag a file here or click to browse</span>\n )}\n <label className=\"cursor-pointer text-xs text-primary hover:underline\">\n {filename ? 'Choose another file' : 'Browse'}\n <input\n id={inputId}\n type=\"file\"\n className=\"sr-only\"\n disabled={isDisabled}\n aria-invalid={hasError || undefined}\n aria-describedby={describedBy}\n onChange={(e) => handleFiles(e.target.files)}\n />\n </label>\n </section>\n );\n}\n","/**\n * `<ImageInput>` — image picker with preview.\n *\n * Phase 1 ships preview-only (URL or local `URL.createObjectURL`).\n * Crop / aspect-ratio editor lands in Phase 2 with `react-image-crop`.\n */\n\nimport type { ImageFieldSchema } from '@arqel-dev/types/fields';\nimport type { FieldRendererProps } from '@arqel-dev/ui/form';\nimport { cn } from '@arqel-dev/ui/utils';\nimport { useEffect, useState } from 'react';\n\nexport function ImageInput({\n field,\n value,\n onChange,\n errors,\n disabled,\n inputId,\n describedBy,\n}: FieldRendererProps) {\n const f = field as ImageFieldSchema;\n const hasError = errors !== undefined && errors.length > 0;\n const isDisabled = disabled || f.disabled || f.readonly;\n\n const [previewUrl, setPreviewUrl] = useState<string | null>(null);\n\n useEffect(() => {\n if (value instanceof File) {\n const url = URL.createObjectURL(value);\n setPreviewUrl(url);\n return () => URL.revokeObjectURL(url);\n }\n if (typeof value === 'string' && value.length > 0) {\n setPreviewUrl(value);\n return undefined;\n }\n setPreviewUrl(null);\n return undefined;\n }, [value]);\n\n return (\n <div className=\"flex flex-col gap-2\">\n {previewUrl && (\n <img\n src={previewUrl}\n alt=\"Preview\"\n className={cn('max-h-40 w-auto rounded-sm border border-border')}\n />\n )}\n <label\n className={cn(\n 'inline-flex w-fit cursor-pointer items-center gap-2 rounded-sm border border-border px-3 py-1.5 text-sm',\n 'hover:bg-muted',\n isDisabled && 'cursor-not-allowed opacity-50',\n )}\n >\n {previewUrl ? 'Replace image' : 'Choose image'}\n <input\n id={inputId}\n type=\"file\"\n accept=\"image/*\"\n className=\"sr-only\"\n disabled={isDisabled}\n aria-invalid={hasError || undefined}\n aria-describedby={describedBy}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) onChange(file);\n }}\n />\n </label>\n </div>\n );\n}\n"]}