akanjs 3.0.0-alpha.37 → 3.0.0-alpha.38
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/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/store/agent/ScreenReader.ts +14 -4
- package/store/agentic/useFieldTool.ts +60 -4
- package/store/agentic/useFormTools.ts +13 -2
- package/store/formSetterNames.ts +5 -0
- package/types/store/agentic/useFieldTool.d.ts +14 -2
- package/types/store/agentic/useFormTools.d.ts +5 -0
- package/types/store/formSetterNames.d.ts +5 -0
- package/types/ui/Field.d.ts +1 -0
- package/ui/DraggableList.tsx +5 -1
- package/ui/Field.tsx +41 -19
- package/ui/Input.tsx +6 -6
- package/ui/Select.tsx +1 -1
- package/ui/Switch.tsx +1 -1
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -248,6 +248,15 @@ export class ScreenReader {
|
|
|
248
248
|
if (href && href !== "#" && !href.startsWith("javascript:") && text !== href) this.#buffer += ` (${href})`;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/**
|
|
252
|
+
* A control the person cannot use publishes no tool, so saying so here is what turns a silent refusal into a
|
|
253
|
+
* fact the agent could have read. It reads `aria-disabled` too: a styled-off div carries no native property.
|
|
254
|
+
*/
|
|
255
|
+
static #off(el: HTMLElement) {
|
|
256
|
+
const native = (el as HTMLInputElement | HTMLButtonElement).disabled;
|
|
257
|
+
return native || el.getAttribute("aria-disabled") === "true" ? " (disabled)" : "";
|
|
258
|
+
}
|
|
259
|
+
|
|
251
260
|
#button(el: HTMLElement) {
|
|
252
261
|
const before = this.#buffer;
|
|
253
262
|
this.#walkChildren(el);
|
|
@@ -255,7 +264,7 @@ export class ScreenReader {
|
|
|
255
264
|
this.#buffer = before;
|
|
256
265
|
const label = inner || el.getAttribute("aria-label") || "";
|
|
257
266
|
const action = el.getAttribute("data-akan-action");
|
|
258
|
-
if (label || action) this.#buffer += ` [button: ${label}${action ? ` → ${action}` : ""}]`;
|
|
267
|
+
if (label || action) this.#buffer += ` [button${ScreenReader.#off(el)}: ${label}${action ? ` → ${action}` : ""}]`;
|
|
259
268
|
}
|
|
260
269
|
|
|
261
270
|
#control(el: HTMLElement, tag: string) {
|
|
@@ -268,12 +277,13 @@ export class ScreenReader {
|
|
|
268
277
|
el.getAttribute("placeholder") ??
|
|
269
278
|
el.getAttribute("name") ??
|
|
270
279
|
type;
|
|
280
|
+
const off = ScreenReader.#off(el);
|
|
271
281
|
if (type === "password") {
|
|
272
|
-
this.#buffer += ` [input ${name}]`;
|
|
282
|
+
this.#buffer += ` [input ${name}${off}]`;
|
|
273
283
|
return;
|
|
274
284
|
}
|
|
275
285
|
if (type === "checkbox" || type === "radio") {
|
|
276
|
-
this.#buffer += ` [${type} ${name}: ${input.checked ? "on" : "off"}]`;
|
|
286
|
+
this.#buffer += ` [${type} ${name}${off}: ${input.checked ? "on" : "off"}]`;
|
|
277
287
|
return;
|
|
278
288
|
}
|
|
279
289
|
const raw =
|
|
@@ -282,7 +292,7 @@ export class ScreenReader {
|
|
|
282
292
|
(el as unknown as HTMLSelectElement).value)
|
|
283
293
|
: input.value;
|
|
284
294
|
const value = (raw ?? "").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
285
|
-
this.#buffer += ` [${tag === "SELECT" ? "select" : "input"} ${name}: ${JSON.stringify(value)}]`;
|
|
295
|
+
this.#buffer += ` [${tag === "SELECT" ? "select" : "input"} ${name}${off}: ${JSON.stringify(value)}]`;
|
|
286
296
|
}
|
|
287
297
|
|
|
288
298
|
#pre(el: HTMLElement) {
|
|
@@ -89,24 +89,77 @@ const rowEntries = (ref: FormFieldRef, arraySchema: JsonSchema): ToolEntry[] =>
|
|
|
89
89
|
];
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
+
export interface FieldToolOptions {
|
|
93
|
+
/** The control's own normalizer. Applied to the agent's write exactly as it is to the person's typing. */
|
|
94
|
+
transform?: unknown;
|
|
95
|
+
/** True while the person cannot use the control. Publishes nothing, so the agent gets no lever the screen withholds. */
|
|
96
|
+
disabled?: boolean;
|
|
97
|
+
/** The person can drag entries into a new order, so `move<Field>On<Model>` is a lever the screen really has. */
|
|
98
|
+
sortable?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Reorder-by-position for a list the person can drag, beside the whole-array setter.
|
|
103
|
+
*
|
|
104
|
+
* The drag is the lever the screen actually offers, and it changes no row's content — so an agent asked to move one
|
|
105
|
+
* row should not have to retype the nine it is leaving alone, which is the same argument that gives an embedded-row
|
|
106
|
+
* array its `add`/`sub`. There is no store action behind it: reordering *is* a whole-array write, so this splices
|
|
107
|
+
* the live rows and hands them to the setter the drag hands them to, `transform` deliberately not applied — the
|
|
108
|
+
* values are already stored, and normalizing them again is not something dragging does.
|
|
109
|
+
*/
|
|
110
|
+
const moveEntry = (ref: FormFieldRef, onChange: () => (value: unknown) => unknown): ToolEntry => {
|
|
111
|
+
const name = formSetterNames(capitalize(ref.refName), ref.key).moveFieldOnModel;
|
|
112
|
+
return {
|
|
113
|
+
name,
|
|
114
|
+
description: `Move one entry of ${ref.key} on the ${ref.refName} form to another position, counting from 0. Reorders only — no entry's content changes.`,
|
|
115
|
+
parameters: {
|
|
116
|
+
type: "object",
|
|
117
|
+
properties: { from: { type: "integer" }, to: { type: "integer" } },
|
|
118
|
+
required: ["from", "to"],
|
|
119
|
+
additionalProperties: false,
|
|
120
|
+
},
|
|
121
|
+
effect: "state",
|
|
122
|
+
guard: (args) => {
|
|
123
|
+
const length = rowsOf(ref).length;
|
|
124
|
+
const outside = ["from", "to"].filter((key) => {
|
|
125
|
+
const idx = args[key];
|
|
126
|
+
return typeof idx !== "number" || !Number.isInteger(idx) || idx < 0 || idx >= length;
|
|
127
|
+
});
|
|
128
|
+
if (!outside.length) return true;
|
|
129
|
+
return `${ref.key} has ${length} ${length === 1 ? "entry" : "entries"}, so ${outside.join(" and ")} is out of range.`;
|
|
130
|
+
},
|
|
131
|
+
run: (args) => {
|
|
132
|
+
const rows = [...rowsOf(ref)];
|
|
133
|
+
const [moved] = rows.splice(args.from as number, 1);
|
|
134
|
+
rows.splice(args.to as number, 0, moved);
|
|
135
|
+
return onChange()(rows);
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
92
140
|
/**
|
|
93
|
-
* Publishes the setter a form control is already holding, for exactly as long as the control is
|
|
141
|
+
* Publishes the setter a form control is already holding, for exactly as long as the control is usable.
|
|
94
142
|
*
|
|
95
143
|
* The control is the declaration — the same rule the rest of the surface follows. A handler passed by reference
|
|
96
144
|
* (`onChange={st.do.setTitleOnTask}`) names the field it writes, so the tool and the person press one function;
|
|
97
145
|
* an inline arrow names nothing and publishes nothing, which is the existing `data-akan-action` rule with
|
|
98
146
|
* consequences. Publishing from the form's subscription instead would offer every field of the model, including
|
|
99
147
|
* the ones this template draws no control for.
|
|
148
|
+
*
|
|
149
|
+
* `disabled` withdraws the tool for the same reason the whole surface is declaration-only: a field the person
|
|
150
|
+
* cannot type into is not one an agent may write in their place. It also closes the field to `fill<Model>Form`,
|
|
151
|
+
* whose guard offers only what a control published — so one gate covers both writers.
|
|
100
152
|
*/
|
|
101
|
-
export const useFieldTool = (onChange: unknown, transform
|
|
153
|
+
export const useFieldTool = (onChange: unknown, { transform, disabled, sortable }: FieldToolOptions = {}) => {
|
|
102
154
|
const surface = useSurface();
|
|
103
155
|
const scope = useScopePath();
|
|
104
156
|
const action = actionTagOf(onChange)?.action ?? null;
|
|
157
|
+
const off = !!disabled;
|
|
105
158
|
const live = useRef({ onChange, transform });
|
|
106
159
|
live.current = { onChange, transform };
|
|
107
160
|
const scopeKey = scope.join(".");
|
|
108
161
|
useEffect(() => {
|
|
109
|
-
if (!action) return;
|
|
162
|
+
if (!action || off) return;
|
|
110
163
|
const ref = FormFields.ref(action);
|
|
111
164
|
const schema = ref && FormFields.schema(ref.field);
|
|
112
165
|
if (!ref || !schema) return;
|
|
@@ -122,10 +175,13 @@ export const useFieldTool = (onChange: unknown, transform?: unknown) => {
|
|
|
122
175
|
},
|
|
123
176
|
},
|
|
124
177
|
...rowEntries(ref, schema),
|
|
178
|
+
...(sortable && ref.field.arrDepth > 0
|
|
179
|
+
? [moveEntry(ref, () => live.current.onChange as (value: unknown) => unknown)]
|
|
180
|
+
: []),
|
|
125
181
|
];
|
|
126
182
|
const registered = entries.map((entry) => surface.registerTool(scope, entry));
|
|
127
183
|
return () => {
|
|
128
184
|
for (const unregister of registered) unregister();
|
|
129
185
|
};
|
|
130
|
-
}, [surface, scopeKey, action]);
|
|
186
|
+
}, [surface, scopeKey, action, off, !!sortable]);
|
|
131
187
|
};
|
|
@@ -17,6 +17,11 @@ import { FormFields } from "./formFields";
|
|
|
17
17
|
* controls will render. The **guard** is where the screen gets its say: a plain field has to have published its
|
|
18
18
|
* own setter, and a composite is waved through because its rows are written with `writeOn<Model>(path, value)`,
|
|
19
19
|
* which no control can annotate — so this is the one place an agent can reach a field the screen may not show.
|
|
20
|
+
*
|
|
21
|
+
* Registered `shared`, because the entry is a pure function of `refName`: the schema comes from the model, the
|
|
22
|
+
* guard re-reads the live surface, and every `write` reaches the one store instance. So a form put on screen by a
|
|
23
|
+
* shell that subscribes it (`Model.EditModal`) and by the `Template` inside it registers one declaration twice,
|
|
24
|
+
* which is interchangeable in the exact sense `shared` means — not a clash an app could fix by suppressing one.
|
|
20
25
|
*/
|
|
21
26
|
export const useFormTools = (refName: string | null, write: (action: string, value: unknown) => void) => {
|
|
22
27
|
const surface = useSurface();
|
|
@@ -44,6 +49,7 @@ export const useFormTools = (refName: string | null, write: (action: string, val
|
|
|
44
49
|
additionalProperties: false,
|
|
45
50
|
},
|
|
46
51
|
effect: "state",
|
|
52
|
+
shared: true,
|
|
47
53
|
guard: (args) => {
|
|
48
54
|
const keys = Object.keys(args);
|
|
49
55
|
if (!keys.length) return "Name at least one field to fill.";
|
|
@@ -59,9 +65,14 @@ export const useFormTools = (refName: string | null, write: (action: string, val
|
|
|
59
65
|
const patch = Object.entries(args).map(([key, value]) => {
|
|
60
66
|
const entry = byKey.get(key);
|
|
61
67
|
if (!entry) throw new Error(`The ${refName} form has no field "${key}".`);
|
|
62
|
-
return
|
|
68
|
+
return { entry, value: FormFields.checked(name, key, entry.field, value) };
|
|
63
69
|
});
|
|
64
|
-
|
|
70
|
+
|
|
71
|
+
for (const { entry, value } of patch) {
|
|
72
|
+
const control = surface.tool(AgenticSurface.fullName(scope, entry.action), scope);
|
|
73
|
+
if (control) void control.run({ value });
|
|
74
|
+
else live.current(entry.action, value);
|
|
75
|
+
}
|
|
65
76
|
},
|
|
66
77
|
});
|
|
67
78
|
}, [surface, scopeKey, refName]);
|
package/store/formSetterNames.ts
CHANGED
|
@@ -16,6 +16,11 @@ export const formSetterNames = (className: string, key: string) => {
|
|
|
16
16
|
addFieldOnModel: `add${classKeyName}On${className}`,
|
|
17
17
|
subFieldOnModel: `sub${classKeyName}On${className}`,
|
|
18
18
|
addOrSubFieldOnModel: `addOrSub${classKeyName}On${className}`,
|
|
19
|
+
/**
|
|
20
|
+
* The agent tool a drag-sortable list publishes. No store action answers to it: reordering *is* the whole-array
|
|
21
|
+
* write the drag already performs, so the tool splices the live rows and hands them to the same setter.
|
|
22
|
+
*/
|
|
23
|
+
moveFieldOnModel: `move${classKeyName}On${className}`,
|
|
19
24
|
uploadFieldOnModel: `upload${classKeyName}On${className}`,
|
|
20
25
|
/**
|
|
21
26
|
* The optional hook a store declares to run after this field is written.
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
+
export interface FieldToolOptions {
|
|
2
|
+
/** The control's own normalizer. Applied to the agent's write exactly as it is to the person's typing. */
|
|
3
|
+
transform?: unknown;
|
|
4
|
+
/** True while the person cannot use the control. Publishes nothing, so the agent gets no lever the screen withholds. */
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
/** The person can drag entries into a new order, so `move<Field>On<Model>` is a lever the screen really has. */
|
|
7
|
+
sortable?: boolean;
|
|
8
|
+
}
|
|
1
9
|
/**
|
|
2
|
-
* Publishes the setter a form control is already holding, for exactly as long as the control is
|
|
10
|
+
* Publishes the setter a form control is already holding, for exactly as long as the control is usable.
|
|
3
11
|
*
|
|
4
12
|
* The control is the declaration — the same rule the rest of the surface follows. A handler passed by reference
|
|
5
13
|
* (`onChange={st.do.setTitleOnTask}`) names the field it writes, so the tool and the person press one function;
|
|
6
14
|
* an inline arrow names nothing and publishes nothing, which is the existing `data-akan-action` rule with
|
|
7
15
|
* consequences. Publishing from the form's subscription instead would offer every field of the model, including
|
|
8
16
|
* the ones this template draws no control for.
|
|
17
|
+
*
|
|
18
|
+
* `disabled` withdraws the tool for the same reason the whole surface is declaration-only: a field the person
|
|
19
|
+
* cannot type into is not one an agent may write in their place. It also closes the field to `fill<Model>Form`,
|
|
20
|
+
* whose guard offers only what a control published — so one gate covers both writers.
|
|
9
21
|
*/
|
|
10
|
-
export declare const useFieldTool: (onChange: unknown, transform?:
|
|
22
|
+
export declare const useFieldTool: (onChange: unknown, { transform, disabled, sortable }?: FieldToolOptions) => void;
|
|
@@ -10,5 +10,10 @@
|
|
|
10
10
|
* controls will render. The **guard** is where the screen gets its say: a plain field has to have published its
|
|
11
11
|
* own setter, and a composite is waved through because its rows are written with `writeOn<Model>(path, value)`,
|
|
12
12
|
* which no control can annotate — so this is the one place an agent can reach a field the screen may not show.
|
|
13
|
+
*
|
|
14
|
+
* Registered `shared`, because the entry is a pure function of `refName`: the schema comes from the model, the
|
|
15
|
+
* guard re-reads the live surface, and every `write` reaches the one store instance. So a form put on screen by a
|
|
16
|
+
* shell that subscribes it (`Model.EditModal`) and by the `Template` inside it registers one declaration twice,
|
|
17
|
+
* which is interchangeable in the exact sense `shared` means — not a clash an app could fix by suppressing one.
|
|
13
18
|
*/
|
|
14
19
|
export declare const useFormTools: (refName: string | null, write: (action: string, value: unknown) => void) => void;
|
|
@@ -12,6 +12,11 @@ export declare const formSetterNames: (className: string, key: string) => {
|
|
|
12
12
|
addFieldOnModel: string;
|
|
13
13
|
subFieldOnModel: string;
|
|
14
14
|
addOrSubFieldOnModel: string;
|
|
15
|
+
/**
|
|
16
|
+
* The agent tool a drag-sortable list publishes. No store action answers to it: reordering *is* the whole-array
|
|
17
|
+
* write the drag already performs, so the tool splices the live rows and hands them to the same setter.
|
|
18
|
+
*/
|
|
19
|
+
moveFieldOnModel: string;
|
|
15
20
|
uploadFieldOnModel: string;
|
|
16
21
|
/**
|
|
17
22
|
* The optional hook a store declares to run after this field is written.
|
package/types/ui/Field.d.ts
CHANGED
|
@@ -242,6 +242,7 @@ interface DateRangeProps<Nullable extends boolean> {
|
|
|
242
242
|
showTime?: boolean;
|
|
243
243
|
onChangeFrom: (value: Dayjs) => void;
|
|
244
244
|
onChangeTo: (value: Dayjs) => void;
|
|
245
|
+
/** The whole range after either end moves. Fires only once both ends are set — nobody can query a half-open one. */
|
|
245
246
|
onChange?: (from: Dayjs, to: Dayjs) => void;
|
|
246
247
|
}
|
|
247
248
|
interface NumberProps {
|
package/ui/DraggableList.tsx
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
import { config, useSprings } from "@react-spring/web";
|
|
3
3
|
import { useGesture } from "@use-gesture/react";
|
|
4
4
|
import { cn } from "akanjs/client";
|
|
5
|
+
import { useFieldTool } from "akanjs/store";
|
|
5
6
|
import { animated } from "akanjs/ui";
|
|
6
7
|
import { createContext, type ReactElement, type ReactNode, useContext, useRef } from "react";
|
|
7
8
|
import { BiTrash } from "react-icons/bi";
|
|
8
9
|
import { MdDragIndicator } from "react-icons/md";
|
|
10
|
+
import { agentAttrs } from "./agentAttrs";
|
|
9
11
|
import { buttonRecipe } from "./Button";
|
|
10
12
|
import { useUiRecipe } from "./UiOverride";
|
|
11
13
|
|
|
@@ -33,6 +35,8 @@ interface DragListProps<V> {
|
|
|
33
35
|
onRemove: (value: V, idx: number) => void;
|
|
34
36
|
}
|
|
35
37
|
const DragList = <V,>({ className, mode = "vertical", children, onChange, onRemove }: DragListProps<V>) => {
|
|
38
|
+
|
|
39
|
+
useFieldTool(onChange, { sortable: true });
|
|
36
40
|
const refs = useRef<(HTMLDivElement | null)[]>([]);
|
|
37
41
|
const order = useRef(children.map((_, index) => index));
|
|
38
42
|
const clientLengths = useRef(children.map((_, index) => 0));
|
|
@@ -103,7 +107,7 @@ const DragList = <V,>({ className, mode = "vertical", children, onChange, onRemo
|
|
|
103
107
|
});
|
|
104
108
|
|
|
105
109
|
return (
|
|
106
|
-
<div className={cn("isolate flex gap-0", mode === "vertical" && "flex-col", className)}>
|
|
110
|
+
<div {...agentAttrs(onChange)} className={cn("isolate flex gap-0", mode === "vertical" && "flex-col", className)}>
|
|
107
111
|
{springs.map(({ zIndex, shadow, movement, scale }, i) => (
|
|
108
112
|
<animated.div
|
|
109
113
|
ref={(el: HTMLDivElement | null) => {
|
package/ui/Field.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { type DataList, type Dayjs, dayjs, type EnumInstance, isEnum } from "aka
|
|
|
3
3
|
import { cn, usePage } from "akanjs/client";
|
|
4
4
|
import { capitalize, formatPhone, isPhoneNumber, lowerlize } from "akanjs/common";
|
|
5
5
|
import type { SliceMeta } from "akanjs/fetch";
|
|
6
|
-
import { st, useFieldTool, useRelationFieldTool } from "akanjs/store";
|
|
6
|
+
import { actionTagOf, st, tagAction, useFieldTool, useRelationFieldTool } from "akanjs/store";
|
|
7
7
|
import { memo, type ReactNode, useState } from "react";
|
|
8
8
|
import { AiOutlinePlus } from "react-icons/ai";
|
|
9
9
|
import { BiHelpCircle, BiTrash, BiX } from "react-icons/bi";
|
|
@@ -182,7 +182,7 @@ const Text = ({
|
|
|
182
182
|
inputClassName,
|
|
183
183
|
inputStyleType = "bordered",
|
|
184
184
|
}: TextProps) => {
|
|
185
|
-
useFieldTool(onChange, transform);
|
|
185
|
+
useFieldTool(onChange, { transform, disabled });
|
|
186
186
|
const { l } = usePage();
|
|
187
187
|
return (
|
|
188
188
|
<div className={cn("flex flex-col", className)}>
|
|
@@ -248,7 +248,7 @@ const Price = ({
|
|
|
248
248
|
inputClassName,
|
|
249
249
|
inputStyleType = "bordered",
|
|
250
250
|
}: PriceProps) => {
|
|
251
|
-
useFieldTool(onChange, transform);
|
|
251
|
+
useFieldTool(onChange, { transform, disabled });
|
|
252
252
|
const { l } = usePage();
|
|
253
253
|
return (
|
|
254
254
|
<div className={cn("flex flex-col", className)}>
|
|
@@ -316,7 +316,7 @@ const TextArea = ({
|
|
|
316
316
|
cache,
|
|
317
317
|
inputClassName,
|
|
318
318
|
}: TextAreaProps) => {
|
|
319
|
-
useFieldTool(onChange, transform);
|
|
319
|
+
useFieldTool(onChange, { transform, disabled });
|
|
320
320
|
const { l } = usePage();
|
|
321
321
|
return (
|
|
322
322
|
<div className={cn("flex flex-col", className)}>
|
|
@@ -370,7 +370,7 @@ const Switch = ({
|
|
|
370
370
|
onDesc,
|
|
371
371
|
offDesc,
|
|
372
372
|
}: SwitchProps) => {
|
|
373
|
-
useFieldTool(onChange);
|
|
373
|
+
useFieldTool(onChange, { disabled });
|
|
374
374
|
return (
|
|
375
375
|
<div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
|
|
376
376
|
{label ? <Label className={labelClassName} nullable label={label} desc={desc} /> : null}
|
|
@@ -419,7 +419,7 @@ const ToggleSelect = <I extends string | number | boolean | null>({
|
|
|
419
419
|
disabled,
|
|
420
420
|
btnClassName,
|
|
421
421
|
}: ToggleSelectProps<I>) => {
|
|
422
|
-
useFieldTool(onChange);
|
|
422
|
+
useFieldTool(onChange, { disabled });
|
|
423
423
|
const { l } = usePage();
|
|
424
424
|
const isEnumValue = isEnum(items as EnumInstance<string, I>);
|
|
425
425
|
return (
|
|
@@ -477,7 +477,7 @@ const MultiToggleSelect = <I extends string | number | boolean>({
|
|
|
477
477
|
onChange,
|
|
478
478
|
disabled,
|
|
479
479
|
}: MultiToggleSelectProps<I>) => {
|
|
480
|
-
useFieldTool(onChange);
|
|
480
|
+
useFieldTool(onChange, { disabled });
|
|
481
481
|
const { l } = usePage();
|
|
482
482
|
const isEnumValue = isEnum(items as EnumInstance<string, I>);
|
|
483
483
|
return (
|
|
@@ -545,7 +545,7 @@ const TextList = ({
|
|
|
545
545
|
validate,
|
|
546
546
|
inputClassName,
|
|
547
547
|
}: TextListProps) => {
|
|
548
|
-
useFieldTool(onChange, transform);
|
|
548
|
+
useFieldTool(onChange, { transform, disabled, sortable: true });
|
|
549
549
|
const { l } = usePage();
|
|
550
550
|
const recipe = useUiRecipe("button") ?? buttonRecipe;
|
|
551
551
|
return (
|
|
@@ -554,7 +554,10 @@ const TextList = ({
|
|
|
554
554
|
<div className="mb-5 h-full gap-2 rounded-box border border-border p-2">
|
|
555
555
|
<DraggableList
|
|
556
556
|
className="h-full gap-2"
|
|
557
|
-
|
|
557
|
+
|
|
558
|
+
onChange={(sorted: string[]) => {
|
|
559
|
+
onChange(sorted);
|
|
560
|
+
}}
|
|
558
561
|
onRemove={(_, idx) => {
|
|
559
562
|
onChange(value.filter((_, i) => i !== idx));
|
|
560
563
|
}}
|
|
@@ -652,7 +655,7 @@ const Tags = ({
|
|
|
652
655
|
validate,
|
|
653
656
|
inputClassName,
|
|
654
657
|
}: TagsProps) => {
|
|
655
|
-
useFieldTool(onChange, transform);
|
|
658
|
+
useFieldTool(onChange, { transform, disabled });
|
|
656
659
|
const { l } = usePage();
|
|
657
660
|
const badge = useUiRecipe("badge") ?? badgeRecipe;
|
|
658
661
|
const [inputVisible, setInputVisible] = useState(false);
|
|
@@ -784,6 +787,7 @@ interface DateRangeProps<Nullable extends boolean> {
|
|
|
784
787
|
showTime?: boolean;
|
|
785
788
|
onChangeFrom: (value: Dayjs) => void;
|
|
786
789
|
onChangeTo: (value: Dayjs) => void;
|
|
790
|
+
/** The whole range after either end moves. Fires only once both ends are set — nobody can query a half-open one. */
|
|
787
791
|
onChange?: (from: Dayjs, to: Dayjs) => void;
|
|
788
792
|
}
|
|
789
793
|
const DateRange = <Nullable extends boolean>({
|
|
@@ -801,6 +805,24 @@ const DateRange = <Nullable extends boolean>({
|
|
|
801
805
|
onChange,
|
|
802
806
|
showTime,
|
|
803
807
|
}: DateRangeProps<Nullable>) => {
|
|
808
|
+
/**
|
|
809
|
+
* Adds the pair callback to one endpoint setter, carrying that setter's own tag onto the wrapper.
|
|
810
|
+
*
|
|
811
|
+
* The wrapper really does run the setter, so the tag stays a true statement — and wiring `onChange` then costs
|
|
812
|
+
* the endpoint neither its agent tool nor its `data-akan-action`, which a plain closure would both drop.
|
|
813
|
+
*/
|
|
814
|
+
const withPair = (setter: (value: Dayjs) => void, pair: (value: Dayjs) => [Dayjs | null, Dayjs | null]) => {
|
|
815
|
+
if (!onChange) return setter;
|
|
816
|
+
const wrapped = (value: Dayjs) => {
|
|
817
|
+
setter(value);
|
|
818
|
+
const [nextFrom, nextTo] = pair(value);
|
|
819
|
+
if (nextFrom && nextTo) onChange(nextFrom, nextTo);
|
|
820
|
+
};
|
|
821
|
+
const tag = actionTagOf(setter);
|
|
822
|
+
return tag ? tagAction(wrapped, tag) : wrapped;
|
|
823
|
+
};
|
|
824
|
+
const changeFrom = withPair(onChangeFrom, (value) => [value, to]);
|
|
825
|
+
const changeTo = withPair(onChangeTo, (value) => [from, value]);
|
|
804
826
|
return (
|
|
805
827
|
<div className={cn("flex flex-col", className)}>
|
|
806
828
|
{label ? <Label className={labelClassName} nullable={nullable} label={label} desc={desc} /> : null}
|
|
@@ -815,7 +837,7 @@ const DateRange = <Nullable extends boolean>({
|
|
|
815
837
|
value={from}
|
|
816
838
|
max={max}
|
|
817
839
|
min={min}
|
|
818
|
-
onChange={
|
|
840
|
+
onChange={changeFrom}
|
|
819
841
|
/>
|
|
820
842
|
</div>
|
|
821
843
|
<div className="relative flex w-full flex-col items-start gap-2 text-center md:flex-row md:items-center">
|
|
@@ -827,7 +849,7 @@ const DateRange = <Nullable extends boolean>({
|
|
|
827
849
|
value={to}
|
|
828
850
|
max={max}
|
|
829
851
|
min={min}
|
|
830
|
-
onChange={
|
|
852
|
+
onChange={changeTo}
|
|
831
853
|
/>
|
|
832
854
|
</div>
|
|
833
855
|
</div>
|
|
@@ -878,7 +900,7 @@ const Number = ({
|
|
|
878
900
|
formatter,
|
|
879
901
|
parser,
|
|
880
902
|
}: NumberProps) => {
|
|
881
|
-
useFieldTool(onChange, transform);
|
|
903
|
+
useFieldTool(onChange, { transform, disabled });
|
|
882
904
|
const { l } = usePage();
|
|
883
905
|
return (
|
|
884
906
|
<div className={cn("flex flex-col", className)}>
|
|
@@ -950,7 +972,7 @@ const DoubleNumber = ({
|
|
|
950
972
|
validate,
|
|
951
973
|
onPressEnter,
|
|
952
974
|
}: DoubleNumberProps) => {
|
|
953
|
-
useFieldTool(onChange, transform);
|
|
975
|
+
useFieldTool(onChange, { transform, disabled });
|
|
954
976
|
const { l } = usePage();
|
|
955
977
|
return (
|
|
956
978
|
<div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
|
|
@@ -1038,7 +1060,7 @@ const Email = ({
|
|
|
1038
1060
|
inputClassName,
|
|
1039
1061
|
inputStyleType,
|
|
1040
1062
|
}: EmailProps) => {
|
|
1041
|
-
useFieldTool(onChange, transform);
|
|
1063
|
+
useFieldTool(onChange, { transform, disabled });
|
|
1042
1064
|
const { l } = usePage();
|
|
1043
1065
|
return (
|
|
1044
1066
|
<div className={cn("flex flex-col", className)}>
|
|
@@ -1103,7 +1125,7 @@ const Phone = ({
|
|
|
1103
1125
|
onPressEnter,
|
|
1104
1126
|
inputClassName,
|
|
1105
1127
|
}: PhoneProps) => {
|
|
1106
|
-
useFieldTool(onChange, transform);
|
|
1128
|
+
useFieldTool(onChange, { transform, disabled });
|
|
1107
1129
|
const { l } = usePage();
|
|
1108
1130
|
|
|
1109
1131
|
return (
|
|
@@ -1175,7 +1197,7 @@ const Password = ({
|
|
|
1175
1197
|
inputClassName,
|
|
1176
1198
|
showConfirm,
|
|
1177
1199
|
}: PasswordProps) => {
|
|
1178
|
-
useFieldTool(onChange, transform);
|
|
1200
|
+
useFieldTool(onChange, { transform, disabled });
|
|
1179
1201
|
const { l } = usePage();
|
|
1180
1202
|
return (
|
|
1181
1203
|
<div className={cn("flex flex-col", className)}>
|
|
@@ -1384,7 +1406,7 @@ const ParentId = <T extends string, State, Input, Full extends { id: string }, L
|
|
|
1384
1406
|
const modelList = storeUse[namesOfSlice.modelList]() as DataList<Light>;
|
|
1385
1407
|
const modelListLoading = storeUse[namesOfSlice.modelListLoading]() as string | boolean;
|
|
1386
1408
|
|
|
1387
|
-
useFieldTool(onChange);
|
|
1409
|
+
useFieldTool(onChange, { disabled });
|
|
1388
1410
|
|
|
1389
1411
|
return (
|
|
1390
1412
|
<div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
|
|
@@ -1573,7 +1595,7 @@ const ChildrenId = <T extends string, State, Input, Full extends { id: string },
|
|
|
1573
1595
|
const modelList = storeUse[namesOfSlice.modelList]() as DataList<Light>;
|
|
1574
1596
|
const modelListLoading = storeUse[namesOfSlice.modelListLoading]() as string | boolean;
|
|
1575
1597
|
|
|
1576
|
-
useFieldTool(onChange);
|
|
1598
|
+
useFieldTool(onChange, { disabled });
|
|
1577
1599
|
|
|
1578
1600
|
return (
|
|
1579
1601
|
<div {...agentAttrs(onChange)} className={cn("flex flex-col", className)}>
|
package/ui/Input.tsx
CHANGED
|
@@ -59,7 +59,7 @@ const DefaultInput = ({
|
|
|
59
59
|
validate,
|
|
60
60
|
...rest
|
|
61
61
|
}: InputProps) => {
|
|
62
|
-
useFieldTool(onChange);
|
|
62
|
+
useFieldTool(onChange, { disabled: rest.disabled });
|
|
63
63
|
const { l } = usePage();
|
|
64
64
|
const [firstFocus, setFirstFocus] = useState(true);
|
|
65
65
|
const validateResult = validate ? validate(value) : undefined;
|
|
@@ -168,7 +168,7 @@ const DefaultTextArea = ({
|
|
|
168
168
|
validate,
|
|
169
169
|
...rest
|
|
170
170
|
}: TextAreaProps) => {
|
|
171
|
-
useFieldTool(onChange);
|
|
171
|
+
useFieldTool(onChange, { disabled: rest.disabled });
|
|
172
172
|
const { l } = usePage();
|
|
173
173
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
174
174
|
const validateResult = validate(value);
|
|
@@ -263,7 +263,7 @@ const DefaultPassword = ({
|
|
|
263
263
|
validate,
|
|
264
264
|
...rest
|
|
265
265
|
}: PasswordProps) => {
|
|
266
|
-
useFieldTool(onChange);
|
|
266
|
+
useFieldTool(onChange, { disabled: rest.disabled });
|
|
267
267
|
const { l } = usePage();
|
|
268
268
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
269
269
|
const validateResult = validate(value);
|
|
@@ -380,7 +380,7 @@ const DefaultEmail = ({
|
|
|
380
380
|
inputWrapperClassName,
|
|
381
381
|
...rest
|
|
382
382
|
}: EmailProps) => {
|
|
383
|
-
useFieldTool(onChange);
|
|
383
|
+
useFieldTool(onChange, { disabled: rest.disabled });
|
|
384
384
|
const { l } = usePage();
|
|
385
385
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
386
386
|
const isValidEmail = isEmail(value);
|
|
@@ -495,7 +495,7 @@ const DefaultNumber = ({
|
|
|
495
495
|
parser,
|
|
496
496
|
...rest
|
|
497
497
|
}: NumberProps) => {
|
|
498
|
-
useFieldTool(onChange);
|
|
498
|
+
useFieldTool(onChange, { disabled: rest.disabled });
|
|
499
499
|
const { l } = usePage();
|
|
500
500
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
501
501
|
const validateResult = validate ? validate(value) : undefined;
|
|
@@ -639,7 +639,7 @@ export type CheckboxProps = Omit<InputHTMLAttributes<HTMLInputElement>, "onChang
|
|
|
639
639
|
};
|
|
640
640
|
|
|
641
641
|
const DefaultCheckbox = ({ checked, onChange, className, ...rest }: CheckboxProps) => {
|
|
642
|
-
useFieldTool(onChange);
|
|
642
|
+
useFieldTool(onChange, { disabled: rest.disabled });
|
|
643
643
|
return (
|
|
644
644
|
<input
|
|
645
645
|
{...rest}
|
package/ui/Select.tsx
CHANGED
|
@@ -85,7 +85,7 @@ const DefaultSelect = <
|
|
|
85
85
|
renderOption,
|
|
86
86
|
renderSelected,
|
|
87
87
|
}: SelectProps<T, Multiple, Searchable, Option>) => {
|
|
88
|
-
useFieldTool(onChange);
|
|
88
|
+
useFieldTool(onChange, { disabled });
|
|
89
89
|
const { l } = usePage();
|
|
90
90
|
const [isOpen, setIsOpen] = useState(false);
|
|
91
91
|
const labeledOptions: { label: string | boolean | number; value: T }[] = useMemo(
|
package/ui/Switch.tsx
CHANGED
|
@@ -32,7 +32,7 @@ export const Switch = ({
|
|
|
32
32
|
className,
|
|
33
33
|
variant = "primary",
|
|
34
34
|
}: SwitchProps) => {
|
|
35
|
-
useFieldTool(onChange);
|
|
35
|
+
useFieldTool(onChange, { disabled });
|
|
36
36
|
const [internal, setInternal] = useState(defaultChecked ?? false);
|
|
37
37
|
const isControlled = checked !== undefined;
|
|
38
38
|
const isChecked = isControlled ? checked : internal;
|