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.
Files changed (58) hide show
  1. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  2. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  3. package/package.json +1 -1
  4. package/store/agentic/StToolBuilder.ts +6 -0
  5. package/store/agentic/formFields.ts +12 -0
  6. package/store/agentic/index.ts +1 -0
  7. package/store/agentic/useFieldTool.ts +105 -14
  8. package/store/agentic/useRelationFieldTool.ts +114 -0
  9. package/types/store/agentic/StToolBuilder.d.ts +5 -0
  10. package/types/store/agentic/formFields.d.ts +9 -0
  11. package/types/store/agentic/index.d.ts +1 -0
  12. package/types/store/agentic/useFieldTool.d.ts +1 -1
  13. package/types/store/agentic/useRelationFieldTool.d.ts +27 -0
  14. package/types/ui/Dialog/LegacyModal.d.ts +13 -0
  15. package/types/ui/Dialog/context.d.ts +6 -0
  16. package/types/ui/Dialog/index.d.ts +1 -0
  17. package/types/ui/Dropdown.d.ts +3 -1
  18. package/types/ui/LegacyModal.d.ts +24 -0
  19. package/types/ui/Model/New.d.ts +3 -1
  20. package/types/ui/Model/NewWrapper.d.ts +2 -0
  21. package/types/ui/Model/NewWrapper_Client.d.ts +2 -1
  22. package/types/ui/index.d.ts +1 -0
  23. package/types/ui/overlayLayer.d.ts +13 -0
  24. package/types/ui/overlayPosition.d.ts +28 -0
  25. package/types/vendor/use-agentic/AgenticSurface.d.ts +6 -0
  26. package/types/vendor/use-agentic/types.d.ts +2 -0
  27. package/types/webkit/index.d.ts +1 -0
  28. package/types/webkit/useBodyScrollLock.d.ts +2 -0
  29. package/ui/Data/ListContainer.tsx +3 -19
  30. package/ui/Dialog/LegacyModal.tsx +241 -0
  31. package/ui/Dialog/Modal.tsx +60 -188
  32. package/ui/Dialog/Provider.tsx +19 -3
  33. package/ui/Dialog/context.ts +7 -0
  34. package/ui/Dialog/index.tsx +2 -0
  35. package/ui/Dropdown.tsx +55 -21
  36. package/ui/Field.tsx +42 -22
  37. package/ui/LegacyModal.tsx +53 -0
  38. package/ui/Model/EditModal.tsx +27 -3
  39. package/ui/Model/EditWrapper.tsx +19 -4
  40. package/ui/Model/New.tsx +4 -0
  41. package/ui/Model/NewWrapper.tsx +2 -0
  42. package/ui/Model/NewWrapper_Client.tsx +21 -6
  43. package/ui/Model/Remove.tsx +17 -6
  44. package/ui/Model/RemoveWrapper.tsx +11 -2
  45. package/ui/Model/SureToRemove.tsx +21 -6
  46. package/ui/Model/ViewEditModal.tsx +67 -15
  47. package/ui/Model/ViewModal.tsx +26 -11
  48. package/ui/Model/ViewWrapper.tsx +14 -4
  49. package/ui/Popconfirm.tsx +81 -68
  50. package/ui/Select.tsx +85 -54
  51. package/ui/Signal/Doc.tsx +2 -1
  52. package/ui/index.ts +1 -0
  53. package/ui/overlayLayer.ts +9 -0
  54. package/ui/overlayPosition.ts +104 -0
  55. package/vendor/use-agentic/AgenticSurface.ts +11 -3
  56. package/vendor/use-agentic/types.ts +2 -0
  57. package/webkit/index.ts +1 -0
  58. package/webkit/useBodyScrollLock.tsx +29 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.34",
3
+ "version": "3.0.0-alpha.36",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -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;
@@ -4,5 +4,6 @@ export * from "./readableValue";
4
4
  export * from "./StToolBuilder";
5
5
  export * from "./useFieldTool";
6
6
  export * from "./useFormTools";
7
+ export * from "./useRelationFieldTool";
7
8
  export * from "./useStExpose";
8
9
  export * from "./useStState";
@@ -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 { FormFields } from "./formFields";
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
- return surface.registerTool(scope, {
30
- name: action,
31
- description: `Set ${ref.key} on the ${ref.refName} form.`,
32
- parameters: { type: "object", properties: { value: schema }, required: ["value"], additionalProperties: false },
33
- effect: "state",
34
- run: (args) =>
35
- (live.current as (value: unknown) => unknown)(
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;
@@ -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;
@@ -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 {};
@@ -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. */
@@ -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";
@@ -0,0 +1,2 @@
1
+ /** Hold `document.body` unscrollable while `active`. */
2
+ export declare const useBodyScrollLock: (active: boolean) => void;