@arqel-dev/hooks 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 +21 -0
- package/README.md +44 -0
- package/SKILL.md +122 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +222 -0
- package/dist/index.js.map +1 -0
- package/dist/useAction.d.ts +20 -0
- package/dist/useAction.js +27 -0
- package/dist/useAction.js.map +1 -0
- package/dist/useArqelForm.d.ts +28 -0
- package/dist/useArqelForm.js +26 -0
- package/dist/useArqelForm.js.map +1 -0
- package/dist/useBreakpoint.d.ts +10 -0
- package/dist/useBreakpoint.js +31 -0
- package/dist/useBreakpoint.js.map +1 -0
- package/dist/useCanAccess.d.ts +11 -0
- package/dist/useCanAccess.js +17 -0
- package/dist/useCanAccess.js.map +1 -0
- package/dist/useFieldDependencies.d.ts +19 -0
- package/dist/useFieldDependencies.js +35 -0
- package/dist/useFieldDependencies.js.map +1 -0
- package/dist/useFlash.d.ts +17 -0
- package/dist/useFlash.js +38 -0
- package/dist/useFlash.js.map +1 -0
- package/dist/useNavigation.d.ts +21 -0
- package/dist/useNavigation.js +14 -0
- package/dist/useNavigation.js.map +1 -0
- package/dist/useOptimistic.d.ts +9 -0
- package/dist/useOptimistic.js +10 -0
- package/dist/useOptimistic.js.map +1 -0
- package/dist/useResource.d.ts +25 -0
- package/dist/useResource.js +20 -0
- package/dist/useResource.js.map +1 -0
- package/dist/useResourceUpdates.d.ts +51 -0
- package/dist/useResourceUpdates.js +29 -0
- package/dist/useResourceUpdates.js.map +1 -0
- package/dist/useTable.d.ts +32 -0
- package/dist/useTable.js +49 -0
- package/dist/useTable.js.map +1 -0
- package/package.json +116 -0
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,44 @@
|
|
|
1
|
+
# @arqel-dev/hooks
|
|
2
|
+
|
|
3
|
+
[](../../LICENSE)
|
|
4
|
+
[](https://react.dev)
|
|
5
|
+
[](#)
|
|
6
|
+
|
|
7
|
+
Reusable React hooks for [Arqel](https://arqel.dev) admin panels.
|
|
8
|
+
|
|
9
|
+
## Status
|
|
10
|
+
|
|
11
|
+
🚧 **Pre-alpha** — HOOKS-001 entregue.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @arqel-dev/hooks @arqel-dev/react @arqel-dev/types
|
|
17
|
+
pnpm add @inertiajs/react react react-dom
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Hooks
|
|
21
|
+
|
|
22
|
+
| Hook | Purpose |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `useResource<T>()` | Typed page props + ResourceContext |
|
|
25
|
+
| `useArqelForm({ fields, record })` | Inertia `useForm` com defaults baseados em fields |
|
|
26
|
+
| `useCanAccess(ability, record?)` | Record-level / global ability check (UX only) |
|
|
27
|
+
| `useFlash({ onMessage })` | Flash messages com callback once-per-new-message |
|
|
28
|
+
| `useTable()` | Local state: sort, filters, selection |
|
|
29
|
+
| `useAction(action)` | Invoca Action via `router.visit` |
|
|
30
|
+
| `useFieldDependencies()` | Debounced partial reload em mudanças de campos |
|
|
31
|
+
| `useNavigation()` | Lê `panel.navigation` dos shared props |
|
|
32
|
+
| `useBreakpoint()` | Tailwind v4 breakpoint atual via `matchMedia` |
|
|
33
|
+
| `useArqelOptimistic()` | Wrap de `useOptimistic` React 19 |
|
|
34
|
+
|
|
35
|
+
## Subpath imports
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { useTable } from '@arqel-dev/hooks/useTable';
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Links
|
|
42
|
+
|
|
43
|
+
- [Documentação](https://arqel.dev/docs/hooks) — em construção
|
|
44
|
+
- [PLANNING](../../PLANNING/08-fase-1-mvp.md) — tickets `HOOKS-*`
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# SKILL.md — @arqel-dev/hooks
|
|
2
|
+
|
|
3
|
+
> Contexto canónico para AI agents.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
`@arqel-dev/hooks` consolida hooks reusáveis para Resource pages, formulários, tabelas, ações, navegação e UI utilities. Consome `@arqel-dev/react` (contexts/utils) e `@inertiajs/react` (page props, useForm, router) — não tem componentes, só lógica. Cada hook é um wrapper stateless sobre Inertia shared props ou page props.
|
|
8
|
+
|
|
9
|
+
## Status
|
|
10
|
+
|
|
11
|
+
**Entregue (HOOKS-001..006):**
|
|
12
|
+
|
|
13
|
+
- 11 entry points subpath (tree-shakeable)
|
|
14
|
+
- `useResource<T>()` — page props + ResourceContext merged
|
|
15
|
+
- `useArqelForm({ fields, record, defaults })` — wrap `useForm` Inertia com `buildInitialFormState`
|
|
16
|
+
- `useCanAccess(ability, record?)` — record-level abilities têm precedência sobre globais
|
|
17
|
+
- `useFlash({ onMessage })` — fires callback uma vez por nova mensagem
|
|
18
|
+
- `useTable({ defaultSort, defaultFilters, defaultSelection })` — estado local (URL sync = Phase 2)
|
|
19
|
+
- `useAction(action)` — invoke via `router.visit`
|
|
20
|
+
- `useFieldDependencies({ fields, values, debounceMs })` — partial reload `fields.<name>.options`, debounce 300ms
|
|
21
|
+
- `useNavigation()` — itens de menu derivados de shared props
|
|
22
|
+
- `useBreakpoint()` — matchMedia wrapper SSR-safe
|
|
23
|
+
- `useArqelOptimistic()` (alias `useOptimistic`) — optimistic state com rollback on failure
|
|
24
|
+
- `useResourceUpdates()` — assinatura a updates Inertia partial reload por resource
|
|
25
|
+
- 30 testes Vitest passando
|
|
26
|
+
|
|
27
|
+
**Por chegar (Phase 2):** Zod validation em `useArqelForm.validate()`, URL sync em `useTable`, real Inertia progress events em `useAction`.
|
|
28
|
+
|
|
29
|
+
## Key Contracts
|
|
30
|
+
|
|
31
|
+
### `useNavigation()`
|
|
32
|
+
|
|
33
|
+
Lê `panel.navigation` da shared prop Inertia e devolve a árvore tipada de items de menu.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { useNavigation } from '@arqel-dev/hooks';
|
|
37
|
+
|
|
38
|
+
const { items } = useNavigation();
|
|
39
|
+
// items: NavigationItemPayload[]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`NavigationItemPayload`:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
type NavigationItemPayload = {
|
|
46
|
+
label: string;
|
|
47
|
+
url: string;
|
|
48
|
+
icon?: string; // nome lucide-react
|
|
49
|
+
group?: string; // header/section visual
|
|
50
|
+
sort?: number; // ordenação
|
|
51
|
+
active?: boolean; // server marca match contra request atual
|
|
52
|
+
children?: NavigationItemPayload[];
|
|
53
|
+
};
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Items vêm 100% do backend (`Panel::navigation()`); o hook só faz coerção (defaults para `[]` quando ausente/inválido).
|
|
57
|
+
|
|
58
|
+
### `useResource<T>()`
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const { records, resource, filters, pagination } = useResource<User>();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Merge de `usePage().props` + `useResourceContext()`.
|
|
65
|
+
|
|
66
|
+
### `useArqelForm`
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const form = useArqelForm({
|
|
70
|
+
fields: schema.fields,
|
|
71
|
+
record: editingUser,
|
|
72
|
+
});
|
|
73
|
+
form.setData('email', 'a@b.c');
|
|
74
|
+
form.post('/admin/users');
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `useTable`
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const table = useTable({ defaultSort: { column: 'created_at', direction: 'desc' } });
|
|
81
|
+
table.setSort('name', 'asc');
|
|
82
|
+
table.toggleSelection(record.id);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `useFlash`
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
useFlash({ onMessage: (kind, msg) => toast[kind](msg) });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Outros
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
useCanAccess('users.update', record); // record precedence > global
|
|
95
|
+
useAction(action); // { invoke, processing, progress }
|
|
96
|
+
useFieldDependencies({ fields, values }); // partial reload com debounce
|
|
97
|
+
useBreakpoint(); // { isMobile, isTablet, isDesktop }
|
|
98
|
+
useArqelOptimistic(initial, reducer);
|
|
99
|
+
useResourceUpdates(resourceName);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Conventions
|
|
103
|
+
|
|
104
|
+
- Subpath imports preferidos: `@arqel-dev/hooks/useTable` evita arrastar deps de outros hooks
|
|
105
|
+
- Hooks acessam page props via `usePage()` — apps devem garantir que estão dentro do `<App>` Inertia
|
|
106
|
+
- `useCanAccess` é UX-only: enforcement real é server-side (ADR-017)
|
|
107
|
+
- Stateless: cada hook deriva do Inertia shared/page props; não mantém cache próprio
|
|
108
|
+
|
|
109
|
+
## Anti-patterns
|
|
110
|
+
|
|
111
|
+
- ❌ Usar `useResource` fora de uma página Inertia — `usePage()` lança
|
|
112
|
+
- ❌ Confiar em `useCanAccess` para esconder dados sensíveis — só esconde da UI
|
|
113
|
+
- ❌ Re-renderizar `useArqelForm` com novo array de `fields` a cada render — memoize antes
|
|
114
|
+
- ❌ Múltiplos `useFlash({ onMessage })` em árvores irmãs — duplica toasts; um único callsite no AppShell
|
|
115
|
+
- ❌ Mutar `items` retornado por `useNavigation` — é o payload server, tratar como readonly
|
|
116
|
+
|
|
117
|
+
## Related
|
|
118
|
+
|
|
119
|
+
- Tickets: [`PLANNING/08-fase-1-mvp.md`](../../PLANNING/08-fase-1-mvp.md) §HOOKS-001..006
|
|
120
|
+
- API: [`PLANNING/06-api-react.md`](../../PLANNING/06-api-react.md) §9
|
|
121
|
+
- Source: [`packages-js/hooks/src/`](src/)
|
|
122
|
+
- Tests: [`packages-js/hooks/tests/`](tests/)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { UseActionResult, useAction } from './useAction.js';
|
|
2
|
+
export { UseArqelFormOptions, UseArqelFormResult, useArqelForm } from './useArqelForm.js';
|
|
3
|
+
export { Breakpoint, useBreakpoint } from './useBreakpoint.js';
|
|
4
|
+
export { useCanAccess } from './useCanAccess.js';
|
|
5
|
+
export { UseFieldDependenciesOptions, useFieldDependencies } from './useFieldDependencies.js';
|
|
6
|
+
export { FlashKind, UseFlashOptions, UseFlashResult, useFlash } from './useFlash.js';
|
|
7
|
+
export { NavigationItemPayload, UseNavigationResult, useNavigation } from './useNavigation.js';
|
|
8
|
+
export { useArqelOptimistic } from './useOptimistic.js';
|
|
9
|
+
export { UseResourceResult, useResource } from './useResource.js';
|
|
10
|
+
export { EchoChannelLike, EchoLike, ResourceUpdatePayload, UseResourceUpdatesOptions, useResourceUpdates } from './useResourceUpdates.js';
|
|
11
|
+
export { TableSort, UseTableOptions, UseTableResult, useTable } from './useTable.js';
|
|
12
|
+
import '@arqel-dev/types/actions';
|
|
13
|
+
import '@arqel-dev/types/fields';
|
|
14
|
+
import '@inertiajs/core';
|
|
15
|
+
import '@inertiajs/react';
|
|
16
|
+
import '@arqel-dev/types/inertia';
|
|
17
|
+
import '@arqel-dev/types/resources';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { router, usePage, useForm } from '@inertiajs/react';
|
|
2
|
+
import { useState, useCallback, useMemo, useEffect, useRef, useOptimistic } from 'react';
|
|
3
|
+
import { buildInitialFormState } from '@arqel-dev/react/utils';
|
|
4
|
+
import { useResourceContext } from '@arqel-dev/react/context';
|
|
5
|
+
|
|
6
|
+
// src/useAction.ts
|
|
7
|
+
function useAction(action) {
|
|
8
|
+
const [processing, setProcessing] = useState(false);
|
|
9
|
+
const invoke = useCallback(
|
|
10
|
+
(record, payload = {}) => {
|
|
11
|
+
const url = action.url ?? `/arqel-dev/actions/${action.name}`;
|
|
12
|
+
const method = action.method.toLowerCase();
|
|
13
|
+
const data = record ? { record_id: record.id, ...payload } : payload;
|
|
14
|
+
setProcessing(true);
|
|
15
|
+
router.visit(url, {
|
|
16
|
+
method,
|
|
17
|
+
data,
|
|
18
|
+
preserveScroll: true,
|
|
19
|
+
onFinish: () => setProcessing(false)
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
[action.name, action.url, action.method]
|
|
23
|
+
);
|
|
24
|
+
return { invoke, processing, progress: 0 };
|
|
25
|
+
}
|
|
26
|
+
function useArqelForm(options) {
|
|
27
|
+
const { fields, record = null, defaults } = options;
|
|
28
|
+
const initial = useMemo(() => {
|
|
29
|
+
const recordObj = record;
|
|
30
|
+
const seed = buildInitialFormState([...fields], recordObj);
|
|
31
|
+
return defaults ? { ...seed, ...defaults } : seed;
|
|
32
|
+
}, [fields, record, defaults]);
|
|
33
|
+
const callUseForm = useForm;
|
|
34
|
+
const form = callUseForm(initial);
|
|
35
|
+
return {
|
|
36
|
+
...form,
|
|
37
|
+
fields,
|
|
38
|
+
clientErrors: {},
|
|
39
|
+
validate: () => true,
|
|
40
|
+
validateField: () => true
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
var QUERIES = [
|
|
44
|
+
{ name: "2xl", min: 1536 },
|
|
45
|
+
{ name: "xl", min: 1280 },
|
|
46
|
+
{ name: "lg", min: 1024 },
|
|
47
|
+
{ name: "md", min: 768 },
|
|
48
|
+
{ name: "sm", min: 0 }
|
|
49
|
+
];
|
|
50
|
+
function resolveBreakpoint(width) {
|
|
51
|
+
for (const q of QUERIES) {
|
|
52
|
+
if (width >= q.min) return q.name;
|
|
53
|
+
}
|
|
54
|
+
return "sm";
|
|
55
|
+
}
|
|
56
|
+
function useBreakpoint() {
|
|
57
|
+
const [bp, setBp] = useState("sm");
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (typeof window === "undefined") return;
|
|
60
|
+
const update = () => setBp(resolveBreakpoint(window.innerWidth));
|
|
61
|
+
update();
|
|
62
|
+
window.addEventListener("resize", update);
|
|
63
|
+
return () => window.removeEventListener("resize", update);
|
|
64
|
+
}, []);
|
|
65
|
+
return bp;
|
|
66
|
+
}
|
|
67
|
+
function useCanAccess(ability, record) {
|
|
68
|
+
const page = usePage();
|
|
69
|
+
const props = page.props;
|
|
70
|
+
const recordCan = record?.can;
|
|
71
|
+
if (recordCan && Object.hasOwn(recordCan, ability)) {
|
|
72
|
+
return recordCan[ability] === true;
|
|
73
|
+
}
|
|
74
|
+
const globalCan = props.auth?.can ?? {};
|
|
75
|
+
return Object.hasOwn(globalCan, ability) ? globalCan[ability] === true : false;
|
|
76
|
+
}
|
|
77
|
+
function useFieldDependencies(options) {
|
|
78
|
+
const { fields, values, debounceMs = 300, onDependencyChange } = options;
|
|
79
|
+
const lastValuesRef = useRef({});
|
|
80
|
+
const timersRef = useRef(/* @__PURE__ */ new Map());
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const timers = timersRef.current;
|
|
83
|
+
for (const field of fields) {
|
|
84
|
+
const deps = field.dependsOn;
|
|
85
|
+
if (!deps || deps.length === 0) continue;
|
|
86
|
+
const changed = deps.some((dep) => values[dep] !== lastValuesRef.current[dep]);
|
|
87
|
+
if (!changed) continue;
|
|
88
|
+
const existing = timers.get(field.name);
|
|
89
|
+
if (existing) clearTimeout(existing);
|
|
90
|
+
const handle = setTimeout(() => {
|
|
91
|
+
router.reload({ only: [`fields.${field.name}.options`] });
|
|
92
|
+
onDependencyChange?.(field.name);
|
|
93
|
+
timers.delete(field.name);
|
|
94
|
+
}, debounceMs);
|
|
95
|
+
timers.set(field.name, handle);
|
|
96
|
+
}
|
|
97
|
+
lastValuesRef.current = { ...values };
|
|
98
|
+
return () => {
|
|
99
|
+
for (const handle of timers.values()) clearTimeout(handle);
|
|
100
|
+
timers.clear();
|
|
101
|
+
};
|
|
102
|
+
}, [fields, values, debounceMs, onDependencyChange]);
|
|
103
|
+
}
|
|
104
|
+
var KINDS = ["success", "error", "info", "warning"];
|
|
105
|
+
var EMPTY_FLASH = {
|
|
106
|
+
success: null,
|
|
107
|
+
error: null,
|
|
108
|
+
info: null,
|
|
109
|
+
warning: null
|
|
110
|
+
};
|
|
111
|
+
function useFlash(options = {}) {
|
|
112
|
+
const page = usePage();
|
|
113
|
+
const props = page.props;
|
|
114
|
+
const flash = props.flash ?? EMPTY_FLASH;
|
|
115
|
+
const seenRef = useRef({
|
|
116
|
+
success: null,
|
|
117
|
+
error: null,
|
|
118
|
+
info: null,
|
|
119
|
+
warning: null
|
|
120
|
+
});
|
|
121
|
+
const { onMessage } = options;
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!onMessage) return;
|
|
124
|
+
for (const kind of KINDS) {
|
|
125
|
+
const value = flash[kind];
|
|
126
|
+
if (value && value !== seenRef.current[kind]) {
|
|
127
|
+
seenRef.current[kind] = value;
|
|
128
|
+
onMessage(kind, value);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}, [flash, onMessage]);
|
|
132
|
+
return flash;
|
|
133
|
+
}
|
|
134
|
+
function useNavigation() {
|
|
135
|
+
const page = usePage();
|
|
136
|
+
const props = page.props;
|
|
137
|
+
const panel = props.panel;
|
|
138
|
+
const items = Array.isArray(panel?.navigation) ? panel.navigation : [];
|
|
139
|
+
return { items };
|
|
140
|
+
}
|
|
141
|
+
function useArqelOptimistic(state, reducer) {
|
|
142
|
+
return useOptimistic(state, reducer);
|
|
143
|
+
}
|
|
144
|
+
function useResource() {
|
|
145
|
+
const page = usePage();
|
|
146
|
+
const props = page.props;
|
|
147
|
+
const contextResource = useResourceContext();
|
|
148
|
+
return {
|
|
149
|
+
resource: props.resource ?? contextResource ?? null,
|
|
150
|
+
records: Array.isArray(props.records) ? props.records : null,
|
|
151
|
+
record: props.record ?? null,
|
|
152
|
+
filters: props.filters ?? {},
|
|
153
|
+
props
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
var DEFAULT_EVENT = ".ResourceUpdated";
|
|
157
|
+
function useResourceUpdates(resourceSlug, recordId, options = {}) {
|
|
158
|
+
const { onUpdate, event = DEFAULT_EVENT } = options;
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const echo = typeof window !== "undefined" ? window.Echo : void 0;
|
|
161
|
+
if (!echo) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const channelName = recordId !== void 0 && recordId !== null ? `arqel.${resourceSlug}.${recordId}` : `arqel.${resourceSlug}`;
|
|
165
|
+
const only = recordId !== void 0 && recordId !== null ? ["record"] : ["records"];
|
|
166
|
+
echo.private(channelName).listen(event, (payload) => {
|
|
167
|
+
if (onUpdate) {
|
|
168
|
+
onUpdate(payload);
|
|
169
|
+
}
|
|
170
|
+
router.reload({ only });
|
|
171
|
+
});
|
|
172
|
+
return () => {
|
|
173
|
+
window.Echo?.leave(channelName);
|
|
174
|
+
};
|
|
175
|
+
}, [resourceSlug, recordId, onUpdate, event]);
|
|
176
|
+
}
|
|
177
|
+
function useTable(options = {}) {
|
|
178
|
+
const [sort, setSortState] = useState(options.defaultSort ?? null);
|
|
179
|
+
const [filters, setFilters] = useState(options.defaultFilters ?? {});
|
|
180
|
+
const [selectedIds, setSelectedIds] = useState(
|
|
181
|
+
options.defaultSelection ?? []
|
|
182
|
+
);
|
|
183
|
+
const setSort = useCallback((column, direction = "asc") => {
|
|
184
|
+
setSortState({ column, direction });
|
|
185
|
+
}, []);
|
|
186
|
+
const clearSort = useCallback(() => setSortState(null), []);
|
|
187
|
+
const setFilter = useCallback((name, value) => {
|
|
188
|
+
setFilters((prev) => {
|
|
189
|
+
if (value === void 0 || value === null || value === "") {
|
|
190
|
+
const { [name]: _omit, ...rest } = prev;
|
|
191
|
+
return rest;
|
|
192
|
+
}
|
|
193
|
+
return { ...prev, [name]: value };
|
|
194
|
+
});
|
|
195
|
+
}, []);
|
|
196
|
+
const clearFilters = useCallback(() => setFilters({}), []);
|
|
197
|
+
const toggleSelection = useCallback((id) => {
|
|
198
|
+
setSelectedIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]);
|
|
199
|
+
}, []);
|
|
200
|
+
const selectAll = useCallback((ids) => {
|
|
201
|
+
setSelectedIds([...ids]);
|
|
202
|
+
}, []);
|
|
203
|
+
const clearSelection = useCallback(() => setSelectedIds([]), []);
|
|
204
|
+
const isSelected = useCallback((id) => selectedIds.includes(id), [selectedIds]);
|
|
205
|
+
return {
|
|
206
|
+
sort,
|
|
207
|
+
setSort,
|
|
208
|
+
clearSort,
|
|
209
|
+
filters,
|
|
210
|
+
setFilter,
|
|
211
|
+
clearFilters,
|
|
212
|
+
selectedIds,
|
|
213
|
+
toggleSelection,
|
|
214
|
+
selectAll,
|
|
215
|
+
clearSelection,
|
|
216
|
+
isSelected
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export { useAction, useArqelForm, useArqelOptimistic, useBreakpoint, useCanAccess, useFieldDependencies, useFlash, useNavigation, useResource, useResourceUpdates, useTable };
|
|
221
|
+
//# sourceMappingURL=index.js.map
|
|
222
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useAction.ts","../src/useArqelForm.ts","../src/useBreakpoint.ts","../src/useCanAccess.ts","../src/useFieldDependencies.ts","../src/useFlash.ts","../src/useNavigation.ts","../src/useOptimistic.ts","../src/useResource.ts","../src/useResourceUpdates.ts","../src/useTable.ts"],"names":["useState","useEffect","router","usePage","useRef","useCallback"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `useAction` — invoke an Action via Inertia router.\n *\n * Phase 1 scope: thin wrapper around `router.visit` that exposes\n * `processing` and a `progress` stub (always 0). HOOKS-004 will wire\n * real Inertia progress events.\n */\n\nimport type { ActionSchema } from '@arqel-dev/types/actions';\nimport type { FormDataConvertible } from '@inertiajs/core';\nimport { router } from '@inertiajs/react';\nimport { useCallback, useState } from 'react';\n\nexport interface UseActionResult {\n invoke: (record: { id: string | number } | null, payload?: Record<string, unknown>) => void;\n processing: boolean;\n progress: number;\n}\n\nexport function useAction(action: ActionSchema): UseActionResult {\n const [processing, setProcessing] = useState(false);\n\n const invoke = useCallback(\n (record: { id: string | number } | null, payload: Record<string, unknown> = {}) => {\n const url = action.url ?? `/arqel-dev/actions/${action.name}`;\n const method = action.method.toLowerCase() as 'get' | 'post' | 'put' | 'patch' | 'delete';\n const data = record ? { record_id: record.id, ...payload } : payload;\n\n setProcessing(true);\n router.visit(url, {\n method,\n data: data as Record<string, FormDataConvertible>,\n preserveScroll: true,\n onFinish: () => setProcessing(false),\n });\n },\n [action.name, action.url, action.method],\n );\n\n return { invoke, processing, progress: 0 };\n}\n","/**\n * `useArqelForm` — wraps Inertia `useForm` with Field-aware defaults.\n *\n * Phase 1 scope: seed initial state from `buildInitialFormState(fields, record)`,\n * proxy through Inertia's API, and stub `validate()`/`validateField()` returning\n * `true` until the Zod bridge ships in HOOKS-002 follow-up.\n */\n\nimport { buildInitialFormState } from '@arqel-dev/react/utils';\nimport type { FieldSchema } from '@arqel-dev/types/fields';\nimport type { FormDataConvertible } from '@inertiajs/core';\nimport type { InertiaFormProps } from '@inertiajs/react';\nimport { useForm } from '@inertiajs/react';\nimport { useMemo } from 'react';\n\ntype FormValues = Record<string, FormDataConvertible>;\n\nexport interface UseArqelFormOptions<TRecord> {\n fields: readonly FieldSchema[];\n record?: TRecord | null;\n defaults?: FormValues;\n}\n\nexport interface UseArqelFormExtensions {\n fields: readonly FieldSchema[];\n clientErrors: Record<string, string[]>;\n validate: () => boolean;\n validateField: (name: string) => boolean;\n}\n\nexport type UseArqelFormResult = InertiaFormProps<FormValues> & UseArqelFormExtensions;\n\nexport function useArqelForm<TRecord = Record<string, unknown>>(\n options: UseArqelFormOptions<TRecord>,\n): UseArqelFormResult {\n const { fields, record = null, defaults } = options;\n\n const initial = useMemo<FormValues>(() => {\n const recordObj = record as Record<string, unknown> | null;\n const seed = buildInitialFormState([...fields], recordObj) as FormValues;\n return defaults ? { ...seed, ...defaults } : seed;\n }, [fields, record, defaults]);\n\n // The Inertia useForm generic causes deep instantiation when the value type\n // is `Record<string, FormDataConvertible>`. The runtime call is fine; we just\n // narrow the result to InertiaFormProps<FormValues> via an unknown bridge.\n const callUseForm = useForm as unknown as (data: FormValues) => InertiaFormProps<FormValues>;\n const form = callUseForm(initial);\n\n return {\n ...form,\n fields,\n clientErrors: {},\n validate: () => true,\n validateField: () => true,\n };\n}\n","/**\n * `useBreakpoint` — current Tailwind v4 breakpoint via `matchMedia`.\n *\n * SSR-safe: returns `'sm'` until mount, then subscribes to media queries.\n * Defaults follow Tailwind v4 (sm 640, md 768, lg 1024, xl 1280, 2xl 1536).\n */\n\nimport { useEffect, useState } from 'react';\n\nexport type Breakpoint = 'sm' | 'md' | 'lg' | 'xl' | '2xl';\n\nconst QUERIES: Array<{ name: Breakpoint; min: number }> = [\n { name: '2xl', min: 1536 },\n { name: 'xl', min: 1280 },\n { name: 'lg', min: 1024 },\n { name: 'md', min: 768 },\n { name: 'sm', min: 0 },\n];\n\nfunction resolveBreakpoint(width: number): Breakpoint {\n for (const q of QUERIES) {\n if (width >= q.min) return q.name;\n }\n return 'sm';\n}\n\nexport function useBreakpoint(): Breakpoint {\n const [bp, setBp] = useState<Breakpoint>('sm');\n\n useEffect(() => {\n if (typeof window === 'undefined') return;\n\n const update = () => setBp(resolveBreakpoint(window.innerWidth));\n update();\n window.addEventListener('resize', update);\n return () => window.removeEventListener('resize', update);\n }, []);\n\n return bp;\n}\n","/**\n * `useCanAccess` — UX-only ability check.\n *\n * Reads `auth.can[ability]` from Inertia shared props. When a record is\n * supplied and the record itself exposes a `can: Record<string, boolean>`\n * map (per-record abilities serialised server-side), that wins over the\n * global map. Real enforcement happens server-side via Policies (ADR-017).\n */\n\nimport type { SharedProps } from '@arqel-dev/types/inertia';\nimport { usePage } from '@inertiajs/react';\n\ninterface RecordWithAbilities {\n can?: Record<string, boolean>;\n}\n\nexport function useCanAccess(ability: string, record?: unknown): boolean {\n const page = usePage();\n const props = page.props as unknown as SharedProps;\n const recordCan = (record as RecordWithAbilities | null | undefined)?.can;\n\n if (recordCan && Object.hasOwn(recordCan, ability)) {\n return recordCan[ability] === true;\n }\n\n const globalCan = props.auth?.can ?? {};\n return Object.hasOwn(globalCan, ability) ? globalCan[ability] === true : false;\n}\n","/**\n * `useFieldDependencies` — debounced partial reload when source fields change.\n *\n * Phase 1 scope: detect changes on field values referenced by `dependsOn`\n * descriptors, then `router.reload` with `only: ['fields.<dep>.options']`\n * after a 300 ms debounce.\n */\n\nimport type { FieldSchema } from '@arqel-dev/types/fields';\nimport { router } from '@inertiajs/react';\nimport { useEffect, useRef } from 'react';\n\nexport interface UseFieldDependenciesOptions {\n fields: readonly FieldSchema[];\n values: Record<string, unknown>;\n debounceMs?: number;\n onDependencyChange?: (fieldName: string) => void;\n}\n\nexport function useFieldDependencies(options: UseFieldDependenciesOptions): void {\n const { fields, values, debounceMs = 300, onDependencyChange } = options;\n const lastValuesRef = useRef<Record<string, unknown>>({});\n const timersRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());\n\n useEffect(() => {\n const timers = timersRef.current;\n\n for (const field of fields) {\n const deps = field.dependsOn;\n if (!deps || deps.length === 0) continue;\n\n const changed = deps.some((dep) => values[dep] !== lastValuesRef.current[dep]);\n if (!changed) continue;\n\n const existing = timers.get(field.name);\n if (existing) clearTimeout(existing);\n\n const handle = setTimeout(() => {\n router.reload({ only: [`fields.${field.name}.options`] });\n onDependencyChange?.(field.name);\n timers.delete(field.name);\n }, debounceMs);\n\n timers.set(field.name, handle);\n }\n\n lastValuesRef.current = { ...values };\n\n return () => {\n for (const handle of timers.values()) clearTimeout(handle);\n timers.clear();\n };\n }, [fields, values, debounceMs, onDependencyChange]);\n}\n","/**\n * `useFlash` — read flash messages from Inertia shared props.\n *\n * Optional `onMessage` callback fires once per *new* message of each kind,\n * allowing toast-library integrations without leaking effects elsewhere.\n */\n\nimport type { FlashPayload, SharedProps } from '@arqel-dev/types/inertia';\nimport { usePage } from '@inertiajs/react';\nimport { useEffect, useRef } from 'react';\n\nexport type FlashKind = 'success' | 'error' | 'info' | 'warning';\n\nexport interface UseFlashOptions {\n onMessage?: (kind: FlashKind, message: string) => void;\n}\n\nexport type UseFlashResult = FlashPayload;\n\nconst KINDS: FlashKind[] = ['success', 'error', 'info', 'warning'];\n\nconst EMPTY_FLASH: FlashPayload = {\n success: null,\n error: null,\n info: null,\n warning: null,\n};\n\nexport function useFlash(options: UseFlashOptions = {}): UseFlashResult {\n const page = usePage();\n const props = page.props as unknown as SharedProps;\n const flash = props.flash ?? EMPTY_FLASH;\n const seenRef = useRef<Record<FlashKind, string | null>>({\n success: null,\n error: null,\n info: null,\n warning: null,\n });\n\n const { onMessage } = options;\n\n useEffect(() => {\n if (!onMessage) return;\n for (const kind of KINDS) {\n const value = flash[kind];\n if (value && value !== seenRef.current[kind]) {\n seenRef.current[kind] = value;\n onMessage(kind, value);\n }\n }\n }, [flash, onMessage]);\n\n return flash;\n}\n","/**\n * `useNavigation` — read panel navigation from Inertia shared props.\n *\n * Server emits a flat or grouped tree under `panel.navigation`; this hook\n * normalises it to a `NavigationItem[]`-like shape callsites can render.\n */\n\nimport type { SharedProps } from '@arqel-dev/types/inertia';\nimport { usePage } from '@inertiajs/react';\n\nexport interface NavigationItemPayload {\n label: string;\n url: string;\n icon?: string | null;\n badge?: string | number | null;\n active?: boolean;\n group?: string | null;\n children?: NavigationItemPayload[];\n}\n\nexport interface UseNavigationResult {\n items: NavigationItemPayload[];\n}\n\ninterface PanelWithNav {\n navigation?: NavigationItemPayload[];\n}\n\nexport function useNavigation(): UseNavigationResult {\n const page = usePage();\n const props = page.props as unknown as SharedProps;\n const panel = props.panel as (SharedProps['panel'] & PanelWithNav) | null;\n const items = Array.isArray(panel?.navigation) ? panel.navigation : [];\n return { items };\n}\n","/**\n * `useArqelOptimistic` — thin wrapper over React 19's `useOptimistic`.\n *\n * Renamed (`useArqelOptimistic`) to avoid colliding with React's hook when\n * both are imported in the same file. Reducer signature mirrors React's.\n */\n\nimport { useOptimistic } from 'react';\n\nexport function useArqelOptimistic<TState, TAction>(\n state: TState,\n reducer: (current: TState, action: TAction) => TState,\n): [TState, (action: TAction) => void] {\n return useOptimistic(state, reducer);\n}\n","/**\n * `useResource` — typed access to Inertia page props for a Resource page.\n *\n * Combines `usePage()` (shared props) with the optional ResourceContext\n * provided by `<ResourceLayout>` so callsites can pull `records`, `actions`,\n * `filters`, etc. with a single hook.\n */\n\nimport { useResourceContext } from '@arqel-dev/react/context';\nimport type { ResourceMeta } from '@arqel-dev/types/resources';\nimport { usePage } from '@inertiajs/react';\n\nexport interface UseResourceResult<TRecord = unknown> {\n /** Server-rendered Resource metadata (label, slug, fields, etc.). */\n resource: ResourceMeta | null;\n /** Records collection — `null` on non-index pages. */\n records: TRecord[] | null;\n /** Single record — `null` on index pages. */\n record: TRecord | null;\n /** Active filters as serialised by Table::serialize(). */\n filters: Record<string, unknown>;\n /** Raw page props for escape-hatch use. */\n props: Record<string, unknown>;\n}\n\ninterface ResourcePagePayload<TRecord> {\n resource?: ResourceMeta;\n records?: TRecord[];\n record?: TRecord;\n filters?: Record<string, unknown>;\n}\n\nexport function useResource<TRecord = unknown>(): UseResourceResult<TRecord> {\n const page = usePage();\n const props = page.props as Record<string, unknown> & ResourcePagePayload<TRecord>;\n const contextResource = useResourceContext();\n\n return {\n resource: props.resource ?? contextResource ?? null,\n records: Array.isArray(props.records) ? (props.records as TRecord[]) : null,\n record: (props.record as TRecord | undefined) ?? null,\n filters: (props.filters as Record<string, unknown> | undefined) ?? {},\n props,\n };\n}\n","/**\n * `useResourceUpdates` — subscribe to a Laravel Echo private channel for a\n * resource (or specific record) and trigger a partial Inertia reload when an\n * update event arrives.\n *\n * The hook is intentionally defensive: it only activates when `window.Echo` is\n * present at runtime. If Reverb/Echo is not configured in the host app, the\n * hook becomes a no-op so that consumers can ship pages without crashing in\n * environments where realtime is disabled.\n *\n * Note: the `laravel-echo` package is *not* a dependency of `@arqel-dev/hooks` —\n * we only declare an ambient minimal shape of `window.Echo`.\n */\n\nimport { router } from '@inertiajs/react';\nimport { useEffect } from 'react';\n\n/**\n * Minimal structural type describing the Echo surface we use. Keeping this\n * narrow avoids a hard dependency on `laravel-echo` types and keeps the hook\n * usable with stubs/mocks.\n */\nexport interface EchoChannelLike {\n listen(event: string, callback: (payload: ResourceUpdatePayload) => void): EchoChannelLike;\n}\n\nexport interface EchoLike {\n private(channel: string): EchoChannelLike;\n leave(channel: string): void;\n}\n\ndeclare global {\n interface Window {\n Echo?: EchoLike;\n }\n}\n\n/**\n * Shape of the broadcast payload. We type it loosely because broadcasters can\n * include arbitrary metadata, but expose convenience fields used by the\n * default UI integration.\n */\n// biome-ignore lint/suspicious/noExplicitAny: payload is intentionally open-ended (broadcast metadata is app-defined).\nexport type ResourceUpdatePayload = Record<string, any>;\n\nexport interface UseResourceUpdatesOptions {\n /**\n * Optional callback invoked on each received update with the raw payload.\n * Useful for showing a toast like \"Updated by {user}\".\n */\n onUpdate?: (payload: ResourceUpdatePayload) => void;\n /**\n * Override the broadcast event name. Defaults to `.ResourceUpdated`.\n * The leading `.` opts into Laravel's \"use literal event name\" convention.\n */\n event?: string;\n}\n\nconst DEFAULT_EVENT = '.ResourceUpdated';\n\nexport function useResourceUpdates(\n resourceSlug: string,\n recordId?: string | number,\n options: UseResourceUpdatesOptions = {},\n): void {\n const { onUpdate, event = DEFAULT_EVENT } = options;\n\n useEffect(() => {\n const echo = typeof window !== 'undefined' ? window.Echo : undefined;\n if (!echo) {\n return;\n }\n\n const channelName =\n recordId !== undefined && recordId !== null\n ? `arqel.${resourceSlug}.${recordId}`\n : `arqel.${resourceSlug}`;\n\n const only = recordId !== undefined && recordId !== null ? ['record'] : ['records'];\n\n echo.private(channelName).listen(event, (payload: ResourceUpdatePayload) => {\n if (onUpdate) {\n onUpdate(payload);\n }\n // Note: Inertia v2 reloads always preserve scroll/state by default\n // (see `ReloadOptions` = `Omit<VisitOptions, 'preserveScroll' | 'preserveState'>`).\n router.reload({ only });\n });\n\n return () => {\n window.Echo?.leave(channelName);\n };\n }, [resourceSlug, recordId, onUpdate, event]);\n}\n","/**\n * `useTable` — table state (sort, filters, selection).\n *\n * Phase 1 scope: pure local state. URL sync via Inertia `router.get`\n * lands in HOOKS-004 follow-up once `<ResourceIndex>` exists.\n */\n\nimport { useCallback, useState } from 'react';\n\nexport type SortDirection = 'asc' | 'desc';\n\nexport interface TableSort {\n column: string;\n direction: SortDirection;\n}\n\nexport interface UseTableOptions {\n defaultSort?: TableSort;\n defaultFilters?: Record<string, unknown>;\n defaultSelection?: ReadonlyArray<string | number>;\n}\n\nexport interface UseTableResult {\n sort: TableSort | null;\n setSort: (column: string, direction?: SortDirection) => void;\n clearSort: () => void;\n filters: Record<string, unknown>;\n setFilter: (name: string, value: unknown) => void;\n clearFilters: () => void;\n selectedIds: ReadonlyArray<string | number>;\n toggleSelection: (id: string | number) => void;\n selectAll: (ids: ReadonlyArray<string | number>) => void;\n clearSelection: () => void;\n isSelected: (id: string | number) => boolean;\n}\n\nexport function useTable(options: UseTableOptions = {}): UseTableResult {\n const [sort, setSortState] = useState<TableSort | null>(options.defaultSort ?? null);\n const [filters, setFilters] = useState<Record<string, unknown>>(options.defaultFilters ?? {});\n const [selectedIds, setSelectedIds] = useState<ReadonlyArray<string | number>>(\n options.defaultSelection ?? [],\n );\n\n const setSort = useCallback((column: string, direction: SortDirection = 'asc') => {\n setSortState({ column, direction });\n }, []);\n\n const clearSort = useCallback(() => setSortState(null), []);\n\n const setFilter = useCallback((name: string, value: unknown) => {\n setFilters((prev) => {\n if (value === undefined || value === null || value === '') {\n const { [name]: _omit, ...rest } = prev;\n return rest;\n }\n return { ...prev, [name]: value };\n });\n }, []);\n\n const clearFilters = useCallback(() => setFilters({}), []);\n\n const toggleSelection = useCallback((id: string | number) => {\n setSelectedIds((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));\n }, []);\n\n const selectAll = useCallback((ids: ReadonlyArray<string | number>) => {\n setSelectedIds([...ids]);\n }, []);\n\n const clearSelection = useCallback(() => setSelectedIds([]), []);\n\n const isSelected = useCallback((id: string | number) => selectedIds.includes(id), [selectedIds]);\n\n return {\n sort,\n setSort,\n clearSort,\n filters,\n setFilter,\n clearFilters,\n selectedIds,\n toggleSelection,\n selectAll,\n clearSelection,\n isSelected,\n };\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ActionSchema } from '@arqel-dev/types/actions';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `useAction` — invoke an Action via Inertia router.
|
|
5
|
+
*
|
|
6
|
+
* Phase 1 scope: thin wrapper around `router.visit` that exposes
|
|
7
|
+
* `processing` and a `progress` stub (always 0). HOOKS-004 will wire
|
|
8
|
+
* real Inertia progress events.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
interface UseActionResult {
|
|
12
|
+
invoke: (record: {
|
|
13
|
+
id: string | number;
|
|
14
|
+
} | null, payload?: Record<string, unknown>) => void;
|
|
15
|
+
processing: boolean;
|
|
16
|
+
progress: number;
|
|
17
|
+
}
|
|
18
|
+
declare function useAction(action: ActionSchema): UseActionResult;
|
|
19
|
+
|
|
20
|
+
export { type UseActionResult, useAction };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { router } from '@inertiajs/react';
|
|
2
|
+
import { useState, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
// src/useAction.ts
|
|
5
|
+
function useAction(action) {
|
|
6
|
+
const [processing, setProcessing] = useState(false);
|
|
7
|
+
const invoke = useCallback(
|
|
8
|
+
(record, payload = {}) => {
|
|
9
|
+
const url = action.url ?? `/arqel-dev/actions/${action.name}`;
|
|
10
|
+
const method = action.method.toLowerCase();
|
|
11
|
+
const data = record ? { record_id: record.id, ...payload } : payload;
|
|
12
|
+
setProcessing(true);
|
|
13
|
+
router.visit(url, {
|
|
14
|
+
method,
|
|
15
|
+
data,
|
|
16
|
+
preserveScroll: true,
|
|
17
|
+
onFinish: () => setProcessing(false)
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
[action.name, action.url, action.method]
|
|
21
|
+
);
|
|
22
|
+
return { invoke, processing, progress: 0 };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { useAction };
|
|
26
|
+
//# sourceMappingURL=useAction.js.map
|
|
27
|
+
//# sourceMappingURL=useAction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useAction.ts"],"names":[],"mappings":";;;;AAmBO,SAAS,UAAU,MAAA,EAAuC;AAC/D,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAI,SAAS,KAAK,CAAA;AAElD,EAAA,MAAM,MAAA,GAAS,WAAA;AAAA,IACb,CAAC,MAAA,EAAwC,OAAA,GAAmC,EAAC,KAAM;AACjF,MAAA,MAAM,GAAA,GAAM,MAAA,CAAO,GAAA,IAAO,CAAA,mBAAA,EAAsB,OAAO,IAAI,CAAA,CAAA;AAC3D,MAAA,MAAM,MAAA,GAAS,MAAA,CAAO,MAAA,CAAO,WAAA,EAAY;AACzC,MAAA,MAAM,IAAA,GAAO,SAAS,EAAE,SAAA,EAAW,OAAO,EAAA,EAAI,GAAG,SAAQ,GAAI,OAAA;AAE7D,MAAA,aAAA,CAAc,IAAI,CAAA;AAClB,MAAA,MAAA,CAAO,MAAM,GAAA,EAAK;AAAA,QAChB,MAAA;AAAA,QACA,IAAA;AAAA,QACA,cAAA,EAAgB,IAAA;AAAA,QAChB,QAAA,EAAU,MAAM,aAAA,CAAc,KAAK;AAAA,OACpC,CAAA;AAAA,IACH,CAAA;AAAA,IACA,CAAC,MAAA,CAAO,IAAA,EAAM,MAAA,CAAO,GAAA,EAAK,OAAO,MAAM;AAAA,GACzC;AAEA,EAAA,OAAO,EAAE,MAAA,EAAQ,UAAA,EAAY,QAAA,EAAU,CAAA,EAAE;AAC3C","file":"useAction.js","sourcesContent":["/**\n * `useAction` — invoke an Action via Inertia router.\n *\n * Phase 1 scope: thin wrapper around `router.visit` that exposes\n * `processing` and a `progress` stub (always 0). HOOKS-004 will wire\n * real Inertia progress events.\n */\n\nimport type { ActionSchema } from '@arqel-dev/types/actions';\nimport type { FormDataConvertible } from '@inertiajs/core';\nimport { router } from '@inertiajs/react';\nimport { useCallback, useState } from 'react';\n\nexport interface UseActionResult {\n invoke: (record: { id: string | number } | null, payload?: Record<string, unknown>) => void;\n processing: boolean;\n progress: number;\n}\n\nexport function useAction(action: ActionSchema): UseActionResult {\n const [processing, setProcessing] = useState(false);\n\n const invoke = useCallback(\n (record: { id: string | number } | null, payload: Record<string, unknown> = {}) => {\n const url = action.url ?? `/arqel-dev/actions/${action.name}`;\n const method = action.method.toLowerCase() as 'get' | 'post' | 'put' | 'patch' | 'delete';\n const data = record ? { record_id: record.id, ...payload } : payload;\n\n setProcessing(true);\n router.visit(url, {\n method,\n data: data as Record<string, FormDataConvertible>,\n preserveScroll: true,\n onFinish: () => setProcessing(false),\n });\n },\n [action.name, action.url, action.method],\n );\n\n return { invoke, processing, progress: 0 };\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { FieldSchema } from '@arqel-dev/types/fields';
|
|
2
|
+
import { FormDataConvertible } from '@inertiajs/core';
|
|
3
|
+
import { InertiaFormProps } from '@inertiajs/react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `useArqelForm` — wraps Inertia `useForm` with Field-aware defaults.
|
|
7
|
+
*
|
|
8
|
+
* Phase 1 scope: seed initial state from `buildInitialFormState(fields, record)`,
|
|
9
|
+
* proxy through Inertia's API, and stub `validate()`/`validateField()` returning
|
|
10
|
+
* `true` until the Zod bridge ships in HOOKS-002 follow-up.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type FormValues = Record<string, FormDataConvertible>;
|
|
14
|
+
interface UseArqelFormOptions<TRecord> {
|
|
15
|
+
fields: readonly FieldSchema[];
|
|
16
|
+
record?: TRecord | null;
|
|
17
|
+
defaults?: FormValues;
|
|
18
|
+
}
|
|
19
|
+
interface UseArqelFormExtensions {
|
|
20
|
+
fields: readonly FieldSchema[];
|
|
21
|
+
clientErrors: Record<string, string[]>;
|
|
22
|
+
validate: () => boolean;
|
|
23
|
+
validateField: (name: string) => boolean;
|
|
24
|
+
}
|
|
25
|
+
type UseArqelFormResult = InertiaFormProps<FormValues> & UseArqelFormExtensions;
|
|
26
|
+
declare function useArqelForm<TRecord = Record<string, unknown>>(options: UseArqelFormOptions<TRecord>): UseArqelFormResult;
|
|
27
|
+
|
|
28
|
+
export { type UseArqelFormExtensions, type UseArqelFormOptions, type UseArqelFormResult, useArqelForm };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { buildInitialFormState } from '@arqel-dev/react/utils';
|
|
2
|
+
import { useForm } from '@inertiajs/react';
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
// src/useArqelForm.ts
|
|
6
|
+
function useArqelForm(options) {
|
|
7
|
+
const { fields, record = null, defaults } = options;
|
|
8
|
+
const initial = useMemo(() => {
|
|
9
|
+
const recordObj = record;
|
|
10
|
+
const seed = buildInitialFormState([...fields], recordObj);
|
|
11
|
+
return defaults ? { ...seed, ...defaults } : seed;
|
|
12
|
+
}, [fields, record, defaults]);
|
|
13
|
+
const callUseForm = useForm;
|
|
14
|
+
const form = callUseForm(initial);
|
|
15
|
+
return {
|
|
16
|
+
...form,
|
|
17
|
+
fields,
|
|
18
|
+
clientErrors: {},
|
|
19
|
+
validate: () => true,
|
|
20
|
+
validateField: () => true
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { useArqelForm };
|
|
25
|
+
//# sourceMappingURL=useArqelForm.js.map
|
|
26
|
+
//# sourceMappingURL=useArqelForm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useArqelForm.ts"],"names":[],"mappings":";;;;;AAgCO,SAAS,aACd,OAAA,EACoB;AACpB,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,GAAS,IAAA,EAAM,UAAS,GAAI,OAAA;AAE5C,EAAA,MAAM,OAAA,GAAU,QAAoB,MAAM;AACxC,IAAA,MAAM,SAAA,GAAY,MAAA;AAClB,IAAA,MAAM,OAAO,qBAAA,CAAsB,CAAC,GAAG,MAAM,GAAG,SAAS,CAAA;AACzD,IAAA,OAAO,WAAW,EAAE,GAAG,IAAA,EAAM,GAAG,UAAS,GAAI,IAAA;AAAA,EAC/C,CAAA,EAAG,CAAC,MAAA,EAAQ,MAAA,EAAQ,QAAQ,CAAC,CAAA;AAK7B,EAAA,MAAM,WAAA,GAAc,OAAA;AACpB,EAAA,MAAM,IAAA,GAAO,YAAY,OAAO,CAAA;AAEhC,EAAA,OAAO;AAAA,IACL,GAAG,IAAA;AAAA,IACH,MAAA;AAAA,IACA,cAAc,EAAC;AAAA,IACf,UAAU,MAAM,IAAA;AAAA,IAChB,eAAe,MAAM;AAAA,GACvB;AACF","file":"useArqelForm.js","sourcesContent":["/**\n * `useArqelForm` — wraps Inertia `useForm` with Field-aware defaults.\n *\n * Phase 1 scope: seed initial state from `buildInitialFormState(fields, record)`,\n * proxy through Inertia's API, and stub `validate()`/`validateField()` returning\n * `true` until the Zod bridge ships in HOOKS-002 follow-up.\n */\n\nimport { buildInitialFormState } from '@arqel-dev/react/utils';\nimport type { FieldSchema } from '@arqel-dev/types/fields';\nimport type { FormDataConvertible } from '@inertiajs/core';\nimport type { InertiaFormProps } from '@inertiajs/react';\nimport { useForm } from '@inertiajs/react';\nimport { useMemo } from 'react';\n\ntype FormValues = Record<string, FormDataConvertible>;\n\nexport interface UseArqelFormOptions<TRecord> {\n fields: readonly FieldSchema[];\n record?: TRecord | null;\n defaults?: FormValues;\n}\n\nexport interface UseArqelFormExtensions {\n fields: readonly FieldSchema[];\n clientErrors: Record<string, string[]>;\n validate: () => boolean;\n validateField: (name: string) => boolean;\n}\n\nexport type UseArqelFormResult = InertiaFormProps<FormValues> & UseArqelFormExtensions;\n\nexport function useArqelForm<TRecord = Record<string, unknown>>(\n options: UseArqelFormOptions<TRecord>,\n): UseArqelFormResult {\n const { fields, record = null, defaults } = options;\n\n const initial = useMemo<FormValues>(() => {\n const recordObj = record as Record<string, unknown> | null;\n const seed = buildInitialFormState([...fields], recordObj) as FormValues;\n return defaults ? { ...seed, ...defaults } : seed;\n }, [fields, record, defaults]);\n\n // The Inertia useForm generic causes deep instantiation when the value type\n // is `Record<string, FormDataConvertible>`. The runtime call is fine; we just\n // narrow the result to InertiaFormProps<FormValues> via an unknown bridge.\n const callUseForm = useForm as unknown as (data: FormValues) => InertiaFormProps<FormValues>;\n const form = callUseForm(initial);\n\n return {\n ...form,\n fields,\n clientErrors: {},\n validate: () => true,\n validateField: () => true,\n };\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useBreakpoint` — current Tailwind v4 breakpoint via `matchMedia`.
|
|
3
|
+
*
|
|
4
|
+
* SSR-safe: returns `'sm'` until mount, then subscribes to media queries.
|
|
5
|
+
* Defaults follow Tailwind v4 (sm 640, md 768, lg 1024, xl 1280, 2xl 1536).
|
|
6
|
+
*/
|
|
7
|
+
type Breakpoint = 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
8
|
+
declare function useBreakpoint(): Breakpoint;
|
|
9
|
+
|
|
10
|
+
export { type Breakpoint, useBreakpoint };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/useBreakpoint.ts
|
|
4
|
+
var QUERIES = [
|
|
5
|
+
{ name: "2xl", min: 1536 },
|
|
6
|
+
{ name: "xl", min: 1280 },
|
|
7
|
+
{ name: "lg", min: 1024 },
|
|
8
|
+
{ name: "md", min: 768 },
|
|
9
|
+
{ name: "sm", min: 0 }
|
|
10
|
+
];
|
|
11
|
+
function resolveBreakpoint(width) {
|
|
12
|
+
for (const q of QUERIES) {
|
|
13
|
+
if (width >= q.min) return q.name;
|
|
14
|
+
}
|
|
15
|
+
return "sm";
|
|
16
|
+
}
|
|
17
|
+
function useBreakpoint() {
|
|
18
|
+
const [bp, setBp] = useState("sm");
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (typeof window === "undefined") return;
|
|
21
|
+
const update = () => setBp(resolveBreakpoint(window.innerWidth));
|
|
22
|
+
update();
|
|
23
|
+
window.addEventListener("resize", update);
|
|
24
|
+
return () => window.removeEventListener("resize", update);
|
|
25
|
+
}, []);
|
|
26
|
+
return bp;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { useBreakpoint };
|
|
30
|
+
//# sourceMappingURL=useBreakpoint.js.map
|
|
31
|
+
//# sourceMappingURL=useBreakpoint.js.map
|