akanjs 3.0.0-alpha.34 → 3.0.0-alpha.36
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/agentic/StToolBuilder.ts +6 -0
- package/store/agentic/formFields.ts +12 -0
- package/store/agentic/index.ts +1 -0
- package/store/agentic/useFieldTool.ts +105 -14
- package/store/agentic/useRelationFieldTool.ts +114 -0
- package/types/store/agentic/StToolBuilder.d.ts +5 -0
- package/types/store/agentic/formFields.d.ts +9 -0
- package/types/store/agentic/index.d.ts +1 -0
- package/types/store/agentic/useFieldTool.d.ts +1 -1
- package/types/store/agentic/useRelationFieldTool.d.ts +27 -0
- package/types/ui/Dialog/LegacyModal.d.ts +13 -0
- package/types/ui/Dialog/context.d.ts +6 -0
- package/types/ui/Dialog/index.d.ts +1 -0
- package/types/ui/Dropdown.d.ts +3 -1
- package/types/ui/LegacyModal.d.ts +24 -0
- package/types/ui/Model/New.d.ts +3 -1
- package/types/ui/Model/NewWrapper.d.ts +2 -0
- package/types/ui/Model/NewWrapper_Client.d.ts +2 -1
- package/types/ui/index.d.ts +1 -0
- package/types/ui/overlayLayer.d.ts +13 -0
- package/types/ui/overlayPosition.d.ts +28 -0
- package/types/vendor/use-agentic/AgenticSurface.d.ts +6 -0
- package/types/vendor/use-agentic/types.d.ts +2 -0
- package/types/webkit/index.d.ts +1 -0
- package/types/webkit/useBodyScrollLock.d.ts +2 -0
- package/ui/Data/ListContainer.tsx +3 -19
- package/ui/Dialog/LegacyModal.tsx +241 -0
- package/ui/Dialog/Modal.tsx +60 -188
- package/ui/Dialog/Provider.tsx +19 -3
- package/ui/Dialog/context.ts +7 -0
- package/ui/Dialog/index.tsx +2 -0
- package/ui/Dropdown.tsx +55 -21
- package/ui/Field.tsx +42 -22
- package/ui/LegacyModal.tsx +53 -0
- package/ui/Model/EditModal.tsx +27 -3
- package/ui/Model/EditWrapper.tsx +19 -4
- package/ui/Model/New.tsx +4 -0
- package/ui/Model/NewWrapper.tsx +2 -0
- package/ui/Model/NewWrapper_Client.tsx +21 -6
- package/ui/Model/Remove.tsx +17 -6
- package/ui/Model/RemoveWrapper.tsx +11 -2
- package/ui/Model/SureToRemove.tsx +21 -6
- package/ui/Model/ViewEditModal.tsx +67 -15
- package/ui/Model/ViewModal.tsx +26 -11
- package/ui/Model/ViewWrapper.tsx +14 -4
- package/ui/Popconfirm.tsx +81 -68
- package/ui/Select.tsx +85 -54
- package/ui/Signal/Doc.tsx +2 -1
- package/ui/index.ts +1 -0
- package/ui/overlayLayer.ts +9 -0
- package/ui/overlayPosition.ts +104 -0
- package/vendor/use-agentic/AgenticSurface.ts +11 -3
- package/vendor/use-agentic/types.ts +2 -0
- package/webkit/index.ts +1 -0
- package/webkit/useBodyScrollLock.tsx +29 -0
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -27,6 +27,11 @@ export interface StToolMeta {
|
|
|
27
27
|
effect?: ToolEffect;
|
|
28
28
|
confirm?: ToolConfirm;
|
|
29
29
|
guard?: ToolGuard;
|
|
30
|
+
/**
|
|
31
|
+
* Set by a component that renders once per row. It is only true when the row's id rides in an argument rather
|
|
32
|
+
* than in the closure, which is what makes every row's registration interchangeable.
|
|
33
|
+
*/
|
|
34
|
+
shared?: boolean;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
interface StToolArg {
|
|
@@ -115,6 +120,7 @@ export class StToolBuilder<Args extends unknown[] = []> {
|
|
|
115
120
|
name,
|
|
116
121
|
description: spec.meta.desc,
|
|
117
122
|
effect: spec.meta.effect,
|
|
123
|
+
...(spec.meta.shared ? { shared: true } : {}),
|
|
118
124
|
parameters: StToolBuilder.parametersOf(spec.args),
|
|
119
125
|
|
|
120
126
|
...(spec.meta.confirm === undefined && !name.startsWith("remove")
|
|
@@ -174,6 +174,18 @@ export class FormFields {
|
|
|
174
174
|
return field.arrDepth > 0 || (field.modelRef as unknown) === Map || !!FormFields.#scalarModel(field);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* The embedded model an array field's rows are, or null for every other field.
|
|
179
|
+
*
|
|
180
|
+
* This is the one shape where writing the whole array is a hazard rather than an inconvenience: the agent has to
|
|
181
|
+
* echo every row it is *not* changing, `checked` validates types and not values, so one mistyped row it was never
|
|
182
|
+
* asked to touch is written silently. A relation array is excluded — its rows travel as ids, which are the payload
|
|
183
|
+
* and are refused by name when wrong — and so is an array of primitives, for the same reason.
|
|
184
|
+
*/
|
|
185
|
+
static rowModelOf(field: ConstantField) {
|
|
186
|
+
return field.arrDepth > 0 ? FormFields.#scalarModel(field) : null;
|
|
187
|
+
}
|
|
188
|
+
|
|
177
189
|
static #scalarModel(field: ConstantField) {
|
|
178
190
|
const modelRef = field.modelRef as unknown as Cls;
|
|
179
191
|
if (field.enum || PrimitiveRegistry.has(modelRef)) return null;
|
package/store/agentic/index.ts
CHANGED
|
@@ -1,9 +1,93 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { capitalize } from "akanjs/common";
|
|
3
|
+
import type { JsonSchema, ToolEntry } from "../../vendor/use-agentic";
|
|
2
4
|
import { useScopePath, useSurface } from "../../vendor/use-agentic";
|
|
3
5
|
import { actionTagOf } from "../actionTag";
|
|
6
|
+
import { formSetterNames } from "../formSetterNames";
|
|
4
7
|
|
|
5
8
|
import { useEffect, useRef } from "../hooks";
|
|
6
|
-
import {
|
|
9
|
+
import { StoreRegistry } from "../storeRegistry";
|
|
10
|
+
import { type FormFieldRef, FormFields } from "./formFields";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A control's `transform` is what it does to every value a person types, so an agent's write goes through it too —
|
|
14
|
+
* otherwise a `Field.Phone` stores `010-1234-5678` for the person and the raw digits for the agent. It normalizes
|
|
15
|
+
* one scalar, so an array-valued control (`TextList`, `Tags`, `DoubleNumber`) applies it per element. A cleared
|
|
16
|
+
* nullable field stays null: a normalizer written for a value would turn it into one.
|
|
17
|
+
*/
|
|
18
|
+
const normalized = (value: unknown, transform: unknown): unknown => {
|
|
19
|
+
if (typeof transform !== "function" || value === null) return value;
|
|
20
|
+
const apply = transform as (input: unknown) => unknown;
|
|
21
|
+
return Array.isArray(value) ? value.map((item) => apply(item)) : apply(value);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const dispatcherOf = (action: string) =>
|
|
25
|
+
StoreRegistry.instance.do[action] as ((...args: unknown[]) => unknown) | undefined;
|
|
26
|
+
|
|
27
|
+
const rowsOf = (ref: FormFieldRef): unknown[] => {
|
|
28
|
+
const form = StoreRegistry.instance.get()[`${ref.refName}Form`] as { [key: string]: unknown } | undefined;
|
|
29
|
+
const rows = form?.[ref.key];
|
|
30
|
+
return Array.isArray(rows) ? rows : [];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Append and remove-by-index for an array of embedded rows, beside the whole-array setter.
|
|
35
|
+
*
|
|
36
|
+
* Not new authority: the setter this control already published can produce any array these two can, so they are
|
|
37
|
+
* strictly weaker — which is what makes deriving them from the same field sound. What they add is that neither can
|
|
38
|
+
* touch a row it was not given, so the agent stops having to retype the rows it is leaving alone.
|
|
39
|
+
*
|
|
40
|
+
* Both take a list and act atomically. Removing indices one call at a time would shift the ones not yet removed, so
|
|
41
|
+
* `sub` filters the whole set at once, the way the generated action already does.
|
|
42
|
+
*
|
|
43
|
+
* `add` appends and publishes no insert position: the `+` a person presses always appends, and the framework cannot
|
|
44
|
+
* see the `limit` an app may pass from its own `onAdd`. `addOrSub` is never published — it matches by `indexOf`, so
|
|
45
|
+
* on rows it compares by reference and every toggle would append.
|
|
46
|
+
*/
|
|
47
|
+
const rowEntries = (ref: FormFieldRef, arraySchema: JsonSchema): ToolEntry[] => {
|
|
48
|
+
if (!FormFields.rowModelOf(ref.field)) return [];
|
|
49
|
+
const names = formSetterNames(capitalize(ref.refName), ref.key);
|
|
50
|
+
if (!dispatcherOf(names.addFieldOnModel) || !dispatcherOf(names.subFieldOnModel)) return [];
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
name: names.addFieldOnModel,
|
|
54
|
+
description: `Append rows to ${ref.key} on the ${ref.refName} form. Leaves every existing row untouched.`,
|
|
55
|
+
parameters: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: { values: arraySchema },
|
|
58
|
+
required: ["values"],
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
61
|
+
effect: "state",
|
|
62
|
+
run: (args) => {
|
|
63
|
+
const checked = FormFields.checked(names.addFieldOnModel, "values", ref.field, args.values);
|
|
64
|
+
return dispatcherOf(names.addFieldOnModel)?.(checked);
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: names.subFieldOnModel,
|
|
69
|
+
description: `Remove rows of ${ref.key} from the ${ref.refName} form by their positions, counting from 0.`,
|
|
70
|
+
parameters: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: { idxs: { type: "array", items: { type: "integer" } } },
|
|
73
|
+
required: ["idxs"],
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
},
|
|
76
|
+
effect: "state",
|
|
77
|
+
guard: (args) => {
|
|
78
|
+
const idxs = args.idxs;
|
|
79
|
+
if (!Array.isArray(idxs) || !idxs.length) return `"idxs" of ${names.subFieldOnModel} takes at least one index.`;
|
|
80
|
+
const length = rowsOf(ref).length;
|
|
81
|
+
const outside = idxs.filter(
|
|
82
|
+
(idx) => typeof idx !== "number" || !Number.isInteger(idx) || idx < 0 || idx >= length,
|
|
83
|
+
);
|
|
84
|
+
if (!outside.length) return true;
|
|
85
|
+
return `${ref.key} has ${length} ${length === 1 ? "row" : "rows"}, so ${outside.join(", ")} is out of range.`;
|
|
86
|
+
},
|
|
87
|
+
run: (args) => dispatcherOf(names.subFieldOnModel)?.(args.idxs),
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
};
|
|
7
91
|
|
|
8
92
|
/**
|
|
9
93
|
* Publishes the setter a form control is already holding, for exactly as long as the control is on screen.
|
|
@@ -14,27 +98,34 @@ import { FormFields } from "./formFields";
|
|
|
14
98
|
* consequences. Publishing from the form's subscription instead would offer every field of the model, including
|
|
15
99
|
* the ones this template draws no control for.
|
|
16
100
|
*/
|
|
17
|
-
export const useFieldTool = (onChange: unknown) => {
|
|
101
|
+
export const useFieldTool = (onChange: unknown, transform?: unknown) => {
|
|
18
102
|
const surface = useSurface();
|
|
19
103
|
const scope = useScopePath();
|
|
20
104
|
const action = actionTagOf(onChange)?.action ?? null;
|
|
21
|
-
const live = useRef(onChange);
|
|
22
|
-
live.current = onChange;
|
|
105
|
+
const live = useRef({ onChange, transform });
|
|
106
|
+
live.current = { onChange, transform };
|
|
23
107
|
const scopeKey = scope.join(".");
|
|
24
108
|
useEffect(() => {
|
|
25
109
|
if (!action) return;
|
|
26
110
|
const ref = FormFields.ref(action);
|
|
27
111
|
const schema = ref && FormFields.schema(ref.field);
|
|
28
112
|
if (!ref || !schema) return;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
FormFields.checked(action, "value", ref.field, args.value === undefined ? null : args.value)
|
|
37
|
-
|
|
38
|
-
|
|
113
|
+
const entries: ToolEntry[] = [
|
|
114
|
+
{
|
|
115
|
+
name: action,
|
|
116
|
+
description: `Set ${ref.key} on the ${ref.refName} form.`,
|
|
117
|
+
parameters: { type: "object", properties: { value: schema }, required: ["value"], additionalProperties: false },
|
|
118
|
+
effect: "state",
|
|
119
|
+
run: (args) => {
|
|
120
|
+
const checked = FormFields.checked(action, "value", ref.field, args.value === undefined ? null : args.value);
|
|
121
|
+
return (live.current.onChange as (value: unknown) => unknown)(normalized(checked, live.current.transform));
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
...rowEntries(ref, schema),
|
|
125
|
+
];
|
|
126
|
+
const registered = entries.map((entry) => surface.registerTool(scope, entry));
|
|
127
|
+
return () => {
|
|
128
|
+
for (const unregister of registered) unregister();
|
|
129
|
+
};
|
|
39
130
|
}, [surface, scopeKey, action]);
|
|
40
131
|
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { type Cls, type DataList, PrimitiveRegistry } from "akanjs/base";
|
|
3
|
+
import { capitalize } from "akanjs/common";
|
|
4
|
+
import { type ConstantField, ConstantRegistry } from "akanjs/constant";
|
|
5
|
+
import type { JsonSchema } from "../../vendor/use-agentic";
|
|
6
|
+
import { useScopePath, useSurface } from "../../vendor/use-agentic";
|
|
7
|
+
import { actionTagOf } from "../actionTag";
|
|
8
|
+
|
|
9
|
+
import { useEffect, useRef } from "../hooks";
|
|
10
|
+
import { FormFields } from "./formFields";
|
|
11
|
+
|
|
12
|
+
export interface RelationFieldSource<T extends { id: string }> {
|
|
13
|
+
/** Read live from the store rather than closed over: `load` below changes the list mid-call. */
|
|
14
|
+
read: () => DataList<T>;
|
|
15
|
+
/** Loads the options an agent never opened the dropdown to fetch. */
|
|
16
|
+
load: () => Promise<unknown> | unknown;
|
|
17
|
+
/** How one option reads to a person, so the agent can match what it sees on screen. */
|
|
18
|
+
label: (model: T) => string;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The database model a relation field points at, or null for anything else — a primitive, an enum, a scalar. */
|
|
23
|
+
const relationOf = (field: ConstantField): string | null => {
|
|
24
|
+
const modelRef = field.modelRef as unknown as Cls;
|
|
25
|
+
if (field.fieldType !== "property" || field.enum || !modelRef || PrimitiveRegistry.has(modelRef)) return null;
|
|
26
|
+
const refName = ConstantRegistry.getRefName(modelRef, { allowEmpty: true });
|
|
27
|
+
return refName && ConstantRegistry.database.has(refName) ? refName : null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The two tools a relation picker owes an agent: list the documents it can pick, then pick by id.
|
|
32
|
+
*
|
|
33
|
+
* `FormFields` publishes no schema for a relation, and it is right not to — the form holds the whole related
|
|
34
|
+
* document, so an id would need a lookup the store does not do. The picker is where that lookup lives: it holds
|
|
35
|
+
* the slice list, the loader, and the label each option renders with. So the field reaches an agent from the one
|
|
36
|
+
* component that can resolve it, which is the rule every other control follows — the control is the declaration.
|
|
37
|
+
*
|
|
38
|
+
* Listing is its own tool because loading is its own step for a person too: the options arrive when the dropdown
|
|
39
|
+
* opens, and an agent never opens it. Folding the load into the setter would leave an agent guessing ids in order
|
|
40
|
+
* to learn them from the refusal.
|
|
41
|
+
*/
|
|
42
|
+
export const useRelationFieldTool = <T extends { id: string }>(
|
|
43
|
+
onChange: unknown,
|
|
44
|
+
{ read, load, label, disabled }: RelationFieldSource<T>,
|
|
45
|
+
) => {
|
|
46
|
+
const surface = useSurface();
|
|
47
|
+
const scope = useScopePath();
|
|
48
|
+
const action = actionTagOf(onChange)?.action ?? null;
|
|
49
|
+
const live = useRef({ onChange, read, load, label });
|
|
50
|
+
live.current = { onChange, read, load, label };
|
|
51
|
+
const scopeKey = scope.join(".");
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!action || disabled) return;
|
|
54
|
+
const ref = FormFields.ref(action);
|
|
55
|
+
const target = ref && relationOf(ref.field);
|
|
56
|
+
|
|
57
|
+
if (!ref || !target || FormFields.schema(ref.field)) return;
|
|
58
|
+
const many = ref.field.arrDepth > 0;
|
|
59
|
+
const nullable = !!ref.field.nullable && !many;
|
|
60
|
+
const listName = `load${capitalize(ref.key)}OptionsOn${capitalize(ref.refName)}`;
|
|
61
|
+
const argName = many ? `${ref.key}Ids` : `${ref.key}Id`;
|
|
62
|
+
const id: JsonSchema = { type: "string" };
|
|
63
|
+
const options = () => live.current.read().map((model) => ({ id: model.id, label: live.current.label(model) }));
|
|
64
|
+
const idsIn = (args: Record<string, unknown>): unknown => {
|
|
65
|
+
const value = args[argName];
|
|
66
|
+
if (nullable && (value === null || value === undefined)) return [];
|
|
67
|
+
return many ? value : [value];
|
|
68
|
+
};
|
|
69
|
+
const offList = surface.registerTool(scope, {
|
|
70
|
+
name: listName,
|
|
71
|
+
description: `List the ${target}s the ${ref.refName} form can pick for ${ref.key}, loading them first. Pass an id from it to ${action}.`,
|
|
72
|
+
effect: "query",
|
|
73
|
+
run: async () => {
|
|
74
|
+
await live.current.load();
|
|
75
|
+
return options();
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const offSet = surface.registerTool(scope, {
|
|
79
|
+
name: action,
|
|
80
|
+
description: `Set ${ref.key} on the ${ref.refName} form to ${many ? `${target}s` : `one ${target}`}, by id. Call ${listName} first for the ids.`,
|
|
81
|
+
parameters: {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: { [argName]: many ? { type: "array", items: id } : id },
|
|
84
|
+
...(nullable ? {} : { required: [argName] }),
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
},
|
|
87
|
+
effect: "state",
|
|
88
|
+
guard: (args) => {
|
|
89
|
+
const ids = idsIn(args);
|
|
90
|
+
if (!Array.isArray(ids)) return `"${argName}" of ${action} must be an array of ids.`;
|
|
91
|
+
const list = live.current.read();
|
|
92
|
+
const missing = ids.filter((value) => typeof value !== "string" || !list.get(value));
|
|
93
|
+
if (!missing.length) return true;
|
|
94
|
+
if (!list.length) return `No ${target} is loaded yet. Call ${listName} first for the ids.`;
|
|
95
|
+
return `The ${ref.refName} form offers no ${target} ${missing.join(", ")}. It offers: ${list
|
|
96
|
+
.map((model) => `${model.id} (${live.current.label(model)})`)
|
|
97
|
+
.join(", ")}.`;
|
|
98
|
+
},
|
|
99
|
+
run: (args) => {
|
|
100
|
+
const list = live.current.read();
|
|
101
|
+
const setter = live.current.onChange as (value: unknown) => unknown;
|
|
102
|
+
const picked = (idsIn(args) as string[]).flatMap((value) => {
|
|
103
|
+
const model = list.get(value);
|
|
104
|
+
return model ? [model] : [];
|
|
105
|
+
});
|
|
106
|
+
return setter(many ? picked : (picked[0] ?? null));
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
return () => {
|
|
110
|
+
offList();
|
|
111
|
+
offSet();
|
|
112
|
+
};
|
|
113
|
+
}, [surface, scopeKey, action, disabled]);
|
|
114
|
+
};
|
|
@@ -6,6 +6,11 @@ export interface StToolMeta {
|
|
|
6
6
|
effect?: ToolEffect;
|
|
7
7
|
confirm?: ToolConfirm;
|
|
8
8
|
guard?: ToolGuard;
|
|
9
|
+
/**
|
|
10
|
+
* Set by a component that renders once per row. It is only true when the row's id rides in an argument rather
|
|
11
|
+
* than in the closure, which is what makes every row's registration interchangeable.
|
|
12
|
+
*/
|
|
13
|
+
shared?: boolean;
|
|
9
14
|
}
|
|
10
15
|
interface StToolArg {
|
|
11
16
|
name: string;
|
|
@@ -41,4 +41,13 @@ export declare class FormFields {
|
|
|
41
41
|
static checked(action: string, path: string, field: ConstantField, value: unknown, depth?: number): unknown;
|
|
42
42
|
/** A field no annotated control can ever name: its rows are written through `writeOn<Model>(path, value)`. */
|
|
43
43
|
static isComposite(field: ConstantField): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The embedded model an array field's rows are, or null for every other field.
|
|
46
|
+
*
|
|
47
|
+
* This is the one shape where writing the whole array is a hazard rather than an inconvenience: the agent has to
|
|
48
|
+
* echo every row it is *not* changing, `checked` validates types and not values, so one mistyped row it was never
|
|
49
|
+
* asked to touch is written silently. A relation array is excluded — its rows travel as ids, which are the payload
|
|
50
|
+
* and are refused by name when wrong — and so is an array of primitives, for the same reason.
|
|
51
|
+
*/
|
|
52
|
+
static rowModelOf(field: ConstantField): (new (...args: never[]) => unknown) | null;
|
|
44
53
|
}
|
|
@@ -4,5 +4,6 @@ export * from "./readableValue.d.ts";
|
|
|
4
4
|
export * from "./StToolBuilder.d.ts";
|
|
5
5
|
export * from "./useFieldTool.d.ts";
|
|
6
6
|
export * from "./useFormTools.d.ts";
|
|
7
|
+
export * from "./useRelationFieldTool.d.ts";
|
|
7
8
|
export * from "./useStExpose.d.ts";
|
|
8
9
|
export * from "./useStState.d.ts";
|
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
* consequences. Publishing from the form's subscription instead would offer every field of the model, including
|
|
8
8
|
* the ones this template draws no control for.
|
|
9
9
|
*/
|
|
10
|
-
export declare const useFieldTool: (onChange: unknown) => void;
|
|
10
|
+
export declare const useFieldTool: (onChange: unknown, transform?: unknown) => void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type DataList } from "akanjs/base";
|
|
2
|
+
export interface RelationFieldSource<T extends {
|
|
3
|
+
id: string;
|
|
4
|
+
}> {
|
|
5
|
+
/** Read live from the store rather than closed over: `load` below changes the list mid-call. */
|
|
6
|
+
read: () => DataList<T>;
|
|
7
|
+
/** Loads the options an agent never opened the dropdown to fetch. */
|
|
8
|
+
load: () => Promise<unknown> | unknown;
|
|
9
|
+
/** How one option reads to a person, so the agent can match what it sees on screen. */
|
|
10
|
+
label: (model: T) => string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The two tools a relation picker owes an agent: list the documents it can pick, then pick by id.
|
|
15
|
+
*
|
|
16
|
+
* `FormFields` publishes no schema for a relation, and it is right not to — the form holds the whole related
|
|
17
|
+
* document, so an id would need a lookup the store does not do. The picker is where that lookup lives: it holds
|
|
18
|
+
* the slice list, the loader, and the label each option renders with. So the field reaches an agent from the one
|
|
19
|
+
* component that can resolve it, which is the rule every other control follows — the control is the declaration.
|
|
20
|
+
*
|
|
21
|
+
* Listing is its own tool because loading is its own step for a person too: the options arrive when the dropdown
|
|
22
|
+
* opens, and an agent never opens it. Folding the load into the setter would leave an agent guessing ids in order
|
|
23
|
+
* to learn them from the refusal.
|
|
24
|
+
*/
|
|
25
|
+
export declare const useRelationFieldTool: <T extends {
|
|
26
|
+
id: string;
|
|
27
|
+
}>(onChange: unknown, { read, load, label, disabled }: RelationFieldSource<T>) => void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface LegacyModalProps {
|
|
3
|
+
className?: string;
|
|
4
|
+
bodyClassName?: string;
|
|
5
|
+
confirmClose?: boolean;
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
onCancel?: () => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Previous modal skin, kept for screens built around its motion: spring open/close and a drag-to-dismiss
|
|
11
|
+
* sheet on touch. New work composes {@link Modal}, which draws the same slots with no animation.
|
|
12
|
+
*/
|
|
13
|
+
export declare const LegacyModal: ({ className, bodyClassName, confirmClose, children, onCancel }: LegacyModalProps) => import("react").ReactPortal | null;
|
|
@@ -4,6 +4,12 @@ export interface DialogContextType {
|
|
|
4
4
|
setOpen: (open: boolean) => void;
|
|
5
5
|
openDialog: () => void;
|
|
6
6
|
closeDialog: () => void;
|
|
7
|
+
/**
|
|
8
|
+
* How this dialog actually dismisses, handed up by whichever surface is drawing it. `confirmClose` and
|
|
9
|
+
* `onCancel` hang off that path, so a close that only flipped `open` would skip both — which is what made an
|
|
10
|
+
* agent's close, and `Dialog.Close`, quietly different from clicking the X.
|
|
11
|
+
*/
|
|
12
|
+
registerDismiss: (dismiss: (() => void) | null) => void;
|
|
7
13
|
title: ReactNode;
|
|
8
14
|
setTitle: (title: ReactNode) => void;
|
|
9
15
|
action: ReactNode;
|
|
@@ -2,6 +2,7 @@ import { type ProviderProps } from "./Provider.d.ts";
|
|
|
2
2
|
export declare const Dialog: {
|
|
3
3
|
({ children, ...props }: ProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
4
4
|
Modal: ({ className, bodyClassName, confirmClose, children, onCancel }: import("./Modal.d.ts").ModalProps) => import("react").ReactPortal | null;
|
|
5
|
+
LegacyModal: ({ className, bodyClassName, confirmClose, children, onCancel }: import("./LegacyModal.d.ts").LegacyModalProps) => import("react").ReactPortal | null;
|
|
5
6
|
Title: ({ children }: import("./Title.d.ts").TitleProps) => null;
|
|
6
7
|
Action: ({ children }: import("./Action.d.ts").ActionProps) => null;
|
|
7
8
|
Trigger: ({ className, children }: import("./Trigger.d.ts").TriggerProps) => import("react/jsx-runtime").JSX.Element;
|
package/types/ui/Dropdown.d.ts
CHANGED
|
@@ -12,8 +12,10 @@ export interface DropdownProps {
|
|
|
12
12
|
buttonClassName?: string;
|
|
13
13
|
/** Additional classes for the dropdown content panel. */
|
|
14
14
|
dropdownClassName?: string;
|
|
15
|
+
/** Trigger edge the menu lines up with. Position is computed, so a `left-0` class cannot do this. */
|
|
16
|
+
align?: "start" | "end";
|
|
15
17
|
}
|
|
16
|
-
export declare const DefaultDropdown: ({ value, content, className, buttonClassName, dropdownClassName }: DropdownProps) => import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export declare const DefaultDropdown: ({ value, content, className, buttonClassName, dropdownClassName, align, }: DropdownProps) => import("react/jsx-runtime").JSX.Element;
|
|
17
19
|
/**
|
|
18
20
|
* Dropdown. Resolves to a route-scoped override when a `page/**\/_overrides.tsx`
|
|
19
21
|
* in the route's ancestry declares one, otherwise renders {@link DefaultDropdown}.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface LegacyModalProps {
|
|
3
|
+
/** Additional classes for the modal surface. */
|
|
4
|
+
className?: string;
|
|
5
|
+
/** Optional modal title. */
|
|
6
|
+
title?: string | ReactNode;
|
|
7
|
+
/** Optional action area, usually footer buttons. */
|
|
8
|
+
action?: ReactNode;
|
|
9
|
+
/** Controlled open state. */
|
|
10
|
+
open: boolean;
|
|
11
|
+
/** Called when the modal requests closing. */
|
|
12
|
+
onCancel: () => void;
|
|
13
|
+
/** Additional classes for the content body. */
|
|
14
|
+
bodyClassName?: string;
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
/** Ask for close confirmation before dismissing. */
|
|
17
|
+
confirmClose?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Previous akanjs modal skin: spring open/close and a drag-to-dismiss sheet on touch. Kept for screens
|
|
21
|
+
* built around that motion. New work uses {@link Modal}, which takes the same props with no animation,
|
|
22
|
+
* and unlike this one resolves through the `Modal` override slot.
|
|
23
|
+
*/
|
|
24
|
+
export declare const LegacyModal: ({ className, title, action, open, onCancel, bodyClassName, children, confirmClose, }: LegacyModalProps) => import("react/jsx-runtime").JSX.Element;
|
package/types/ui/Model/New.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ interface NewProps<Full = any> {
|
|
|
11
11
|
renderTitle?: ((model: {
|
|
12
12
|
id: string;
|
|
13
13
|
}) => string | ReactNode) | string;
|
|
14
|
+
/** Suffixes the tool this button publishes. Only a second create button for the same slice needs one. */
|
|
15
|
+
namespace?: string;
|
|
14
16
|
}
|
|
15
|
-
export default function New({ className, wrapperClassName, type, children, slice, modal, partial, renderTitle, }: NewProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export default function New({ className, wrapperClassName, type, children, slice, modal, partial, renderTitle, namespace, }: NewProps): import("react/jsx-runtime").JSX.Element;
|
|
16
18
|
export {};
|
|
@@ -8,6 +8,8 @@ interface NewWrapperProps<Full = any> {
|
|
|
8
8
|
setDefault?: boolean;
|
|
9
9
|
modal?: string | null;
|
|
10
10
|
resets?: string[] | null;
|
|
11
|
+
/** Suffixes the tool this trigger publishes. Only a second create trigger for the same slice needs one. */
|
|
12
|
+
namespace?: string;
|
|
11
13
|
}
|
|
12
14
|
export default function NewWrapper<Full>({ partial, ...props }: NewWrapperProps<Full>): import("react/jsx-runtime").JSX.Element;
|
|
13
15
|
export {};
|
|
@@ -8,6 +8,7 @@ interface NewWrapperProps<Full = any> {
|
|
|
8
8
|
setDefault?: boolean;
|
|
9
9
|
modal?: string | null;
|
|
10
10
|
resets?: string[] | null;
|
|
11
|
+
namespace?: string;
|
|
11
12
|
}
|
|
12
|
-
export declare const NewWrapper_Client: <Full>({ children, slice, partial, setDefault, className, modal, resets, }: NewWrapperProps<Full>) => import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export declare const NewWrapper_Client: <Full>({ children, slice, partial, setDefault, className, modal, resets, namespace, }: NewWrapperProps<Full>) => import("react/jsx-runtime").JSX.Element;
|
|
13
14
|
export {};
|
package/types/ui/index.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export { InfiniteScroll } from "./InfiniteScroll.d.ts";
|
|
|
23
23
|
export { Input } from "./Input.d.ts";
|
|
24
24
|
export { KeyboardAvoiding } from "./KeyboardAvoiding.d.ts";
|
|
25
25
|
export { Layout } from "./Layout.d.ts";
|
|
26
|
+
export { LegacyModal } from "./LegacyModal.d.ts";
|
|
26
27
|
export { Link } from "./Link.d.ts";
|
|
27
28
|
export { Load } from "./Load.d.ts";
|
|
28
29
|
export { Loading } from "./Loading.d.ts";
|
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
* recognises its own overlays exactly, instead of guessing from how close a dialog happens to be.
|
|
9
9
|
*/
|
|
10
10
|
export declare const OVERLAY_LAYER_ATTR = "data-akan-overlay";
|
|
11
|
+
/**
|
|
12
|
+
* Stacking order for overlays that portal to `document.body`, where nesting no longer decides who is on
|
|
13
|
+
* top. A Popconfirm outranks a dropdown menu because `Model.Remove` draws one from inside a menu item,
|
|
14
|
+
* and its scrim outranks the menu so the click that answers the confirm is not also a menu selection.
|
|
15
|
+
* A Select's options stay under both: it is a field, and whatever opened over it took the click that
|
|
16
|
+
* dismisses it.
|
|
17
|
+
*/
|
|
18
|
+
export declare const overlayZ: {
|
|
19
|
+
readonly select: 90;
|
|
20
|
+
readonly dropdown: 100;
|
|
21
|
+
readonly popconfirmScrim: 105;
|
|
22
|
+
readonly popconfirm: 110;
|
|
23
|
+
};
|
|
11
24
|
/** Wrap the content a dismissable container owns, with the scope from {@link useOverlayScope}. */
|
|
12
25
|
export declare const OverlayOwnerProvider: import("react").Provider<string>;
|
|
13
26
|
/** Scope a dismissable container hands to its content. Nests as a `parent/child` path. */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
export interface OverlayPosition {
|
|
3
|
+
top: number;
|
|
4
|
+
left: number;
|
|
5
|
+
/** Placed above the trigger, because there was no room below. */
|
|
6
|
+
above: boolean;
|
|
7
|
+
/** The trigger's centre relative to the panel's left edge, held clear of the panel's rounded corners. */
|
|
8
|
+
anchorOffset: number;
|
|
9
|
+
/** The trigger's own width, for a panel that lines up with the control it drops out of. */
|
|
10
|
+
anchorWidth: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Places a portalled panel against its trigger in viewport coordinates.
|
|
14
|
+
*
|
|
15
|
+
* A panel positioned inside its own tree is clipped by every `overflow` ancestor it has — the modal
|
|
16
|
+
* surface, the modal's scrolling body, a table's scroll container, the dropdown menu a row verb sits in —
|
|
17
|
+
* so overlay panels portal out to `document.body` and are placed here instead of by CSS.
|
|
18
|
+
* `position: fixed` in place would not do it either: a fixed element inside a transformed ancestor is laid
|
|
19
|
+
* out and clipped by that ancestor, and the modal surface animates a transform.
|
|
20
|
+
*/
|
|
21
|
+
export declare const useOverlayPosition: ({ opened, triggerRef, panelRef, align, gap, }: {
|
|
22
|
+
opened: boolean;
|
|
23
|
+
triggerRef: RefObject<HTMLElement | null>;
|
|
24
|
+
panelRef: RefObject<HTMLElement | null>;
|
|
25
|
+
align: "start" | "end";
|
|
26
|
+
/** Distance from the trigger. Raise it for a panel whose pointer sticks out past its own edge. */
|
|
27
|
+
gap?: number;
|
|
28
|
+
}) => OverlayPosition | null;
|
|
@@ -15,6 +15,12 @@ export declare class AgenticSurface {
|
|
|
15
15
|
static childPath(parent: string[], id: string): string[];
|
|
16
16
|
static fullName(scope: string[], name: string): string;
|
|
17
17
|
subscribe(listener: () => void): () => void;
|
|
18
|
+
/**
|
|
19
|
+
* A row component registers the same name once per row. That is legal when the tool takes the row's id as an
|
|
20
|
+
* argument rather than closing over it, because every registration is then interchangeable and last-wins picks
|
|
21
|
+
* an equivalent one — `shared` is the registrant saying so, and it turns fifty rows from fifty collisions into
|
|
22
|
+
* one declaration. A shared name landing on top of a name nobody shared is still a real clash, and still warns.
|
|
23
|
+
*/
|
|
18
24
|
registerTool(scope: string[], entry: ToolEntry): () => void;
|
|
19
25
|
registerResource(scope: string[], entry: ResourceEntry): () => void;
|
|
20
26
|
openScope(parent: string[], scope: ScopeEntry): () => void;
|
|
@@ -11,6 +11,8 @@ export interface ToolEntry {
|
|
|
11
11
|
effect?: ToolEffect;
|
|
12
12
|
confirm?: ToolConfirm;
|
|
13
13
|
guard?: ToolGuard;
|
|
14
|
+
/** Declares that repeat registrations of this name are interchangeable — see `AgenticSurface.registerTool`. */
|
|
15
|
+
shared?: boolean;
|
|
14
16
|
run: (args: Record<string, unknown>) => unknown;
|
|
15
17
|
}
|
|
16
18
|
/** One call an agent made through the surface, in the order it made them. */
|
package/types/webkit/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { createRobotPage } from "./createRobotPage.d.ts";
|
|
|
3
3
|
export { createSitemapPage } from "./createSitemapPage.d.ts";
|
|
4
4
|
export { lazy } from "./lazy.d.ts";
|
|
5
5
|
export type * from "./types.d.ts";
|
|
6
|
+
export { useBodyScrollLock } from "./useBodyScrollLock.d.ts";
|
|
6
7
|
export { useCamera } from "./useCamera.d.ts";
|
|
7
8
|
export { useCodepush } from "./useCodepush.d.ts";
|
|
8
9
|
export { useContact } from "./useContact.d.ts";
|