@pipelex/mthds-form 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,215 @@
1
+ import * as react from 'react';
2
+ import { FileRunField, RunField } from './core/descriptor.js';
3
+ import { FileSource } from './core/sources.js';
4
+
5
+ /** The serializable value of a file field. URL is what the runner sends. */
6
+ interface FileValue {
7
+ url?: string;
8
+ filename?: string;
9
+ }
10
+ /** What a host's upload resolves to: the stored file's URL, or its URL and name. */
11
+ type UploadedFile = string | {
12
+ url: string;
13
+ filename?: string;
14
+ };
15
+ interface FileFieldProps {
16
+ field: FileRunField;
17
+ value: FileValue | undefined;
18
+ /**
19
+ * Called when a file is dropped/picked - the parent uploads and sets value.
20
+ *
21
+ * It may return the upload: a promise of what it stored, whose URL is the
22
+ * one the host writes into the value. The control then knows its upload's
23
+ * own result by that URL, so the dropped file's preview is kept for it
24
+ * whichever render the host writes it in, and a rejection retires the
25
+ * preview at once. Returning nothing leaves the control to judge the upload
26
+ * by when its value lands (docs/upload-seam.md).
27
+ *
28
+ * Absent means the host cannot store a file, and the control then offers no
29
+ * dropzone and no picker: the link input is open from the start, with a line
30
+ * saying a file cannot be uploaded here.
31
+ */
32
+ onDropFile?: (file: File) => void | PromiseLike<UploadedFile | void>;
33
+ /**
34
+ * Whether the link input is offered, `true` when absent. `false` removes the
35
+ * "paste a URL instead" toggle and the input behind it, and says nothing about
36
+ * which values are valid. With no `onDropFile` either, the control throws.
37
+ */
38
+ allowUrl?: boolean;
39
+ /** Manual URL paste / clear flows through here too. */
40
+ onChange: (value: FileValue | undefined) => void;
41
+ id: string;
42
+ /** Parent-driven async state. */
43
+ uploading?: boolean;
44
+ /**
45
+ * Why the host's upload of this field's file failed. Shown in the alert slot
46
+ * a refused format uses, until the host removes it. A clear or a typed link
47
+ * hides it until the host sends a different message or removes it; a refused
48
+ * pick shows its refusal in its place.
49
+ */
50
+ uploadError?: string;
51
+ /** Resolve a `pipelex-storage://` URI to a viewable URL (stored-file preview). */
52
+ resolveUrl?: (uri: string) => Promise<string | null>;
53
+ /**
54
+ * Where the field can take a file the host already stores. When given, the
55
+ * empty field offers "Choose from your team's files" under its own controls,
56
+ * and the chosen file is written through `onChange` as `{ url, filename }`,
57
+ * the value an upload writes. See docs/sources.md.
58
+ */
59
+ fileSource?: FileSource;
60
+ error?: string;
61
+ disabled?: boolean;
62
+ }
63
+ /** Document variant: a file shown as a PDF preview (or chip for other docs). */
64
+ declare function DocumentField(props: FileFieldProps): react.JSX.Element;
65
+ /** Image variant: a file shown as a real image preview. */
66
+ declare function ImageField(props: FileFieldProps): react.JSX.Element;
67
+
68
+ /**
69
+ * The moves a host makes to a list's rows, and what a move does to an id.
70
+ *
71
+ * A field's id is its value path, so a row's id is its position (`cvs.1`), and
72
+ * moving rows renumbers them. Two parties hold something by row and must move
73
+ * it when the rows move: the host, which keys what it knows about each upload
74
+ * by id, and the list control, which keys each row's React element by an
75
+ * identity of its own so the row's controls travel with it. `useMethodForm`
76
+ * makes every move itself (`removeItem`, `moveItem`), re-keys its own records
77
+ * as it does, and hands the list a log of those moves (`FieldEnv.rowMoves`),
78
+ * which the list replays on its row identities. One log, read by both, is what
79
+ * keeps them agreeing about which row is which. See docs/upload-seam.md, "What
80
+ * the form keeps by id moves with the rows it moves".
81
+ */
82
+ /** One move of a list's rows, as a host records it for the list to replay. */
83
+ interface RowMove {
84
+ /**
85
+ * The move's number: each move is numbered above the one before it, for as
86
+ * long as the host lives, so a list knows which moves it has replayed.
87
+ */
88
+ seq: number;
89
+ /** The list's id, the one its rows' ids extend (`cvs` for `cvs.1`). */
90
+ listId: string;
91
+ /**
92
+ * For each row of the list after the move, the index that row held before
93
+ * it. A row whose index is left out was removed.
94
+ */
95
+ order: readonly number[];
96
+ }
97
+
98
+ /**
99
+ * Ambient state that any field in the tree may need, threaded unchanged through
100
+ * the recursion so containers don't have to re-plumb it for every child.
101
+ */
102
+ interface FieldEnv {
103
+ disabled?: boolean;
104
+ /**
105
+ * A file field asks its parent to upload; the parent sets the value later.
106
+ *
107
+ * It may return the upload: a promise of what it stored (`UploadedFile`),
108
+ * whose URL is the one the host writes at the id, rejected when the upload
109
+ * writes nothing. The field then knows its upload's own result by that URL,
110
+ * so the dropped file's preview is kept for it whichever render the host
111
+ * writes it in. A host that returns nothing must write the result in the
112
+ * render it takes the id out of `uploadingIds`. `useMethodForm`'s `dropFile`
113
+ * returns its upload. See docs/upload-seam.md, "The local preview belongs to
114
+ * the value it was made for".
115
+ *
116
+ * Its presence is the capability. A host that supplies it gets a dropzone on
117
+ * every file field; a host that does not gets no dropzone and no file picker,
118
+ * because a file taken with nowhere to go used to be dropped without a word.
119
+ * See docs/upload-seam.md, "Which ways into a file value a host offers".
120
+ */
121
+ onDropFile?: (id: string, file: File) => void | PromiseLike<UploadedFile | void>;
122
+ /**
123
+ * Whether a file field offers a link input, the way in that needs no upload.
124
+ * `true` when absent; `false` hides the "paste a URL instead" toggle and the
125
+ * input behind it on every file field.
126
+ *
127
+ * It governs that input and nothing else. A file value is a URL whichever way
128
+ * it arrived, so this says nothing about which values are valid: a web link
129
+ * the host writes is still shown on the field's card and can still be
130
+ * cleared. Setting it to `false` with no `onDropFile` leaves a file field no
131
+ * way in at all, and the field throws, naming its path, when it renders.
132
+ */
133
+ allowUrl?: boolean;
134
+ /**
135
+ * Removes a list's row on the host's side: `listId` is the list's id, the
136
+ * one its rows' ids extend (`cvs` for `cvs.1`), and `index` the row's.
137
+ *
138
+ * Its presence is the capability, and it is the host saying it follows
139
+ * rows. A list hands each removal to it instead of writing the shorter list
140
+ * through `onChange`, so the host removes the row and, in the same update,
141
+ * moves to its new id whatever it keeps by the id of a later row (an entry
142
+ * of `uploadErrors`, an upload in flight) and drops what it kept for the
143
+ * removed one. The list then holds back none of the host's `uploadErrors`,
144
+ * which it does for a host without this callback, since that host is
145
+ * handed only the shorter list and cannot re-key them. A host that also
146
+ * gives `rowMoves` records the removal there, and the list moves its rows'
147
+ * identities from that record alone. `useMethodForm`'s `removeItem` is this
148
+ * callback. See docs/upload-seam.md, "A list does not move a failure onto
149
+ * another row".
150
+ */
151
+ onRemoveItem?: (listId: string, index: number) => void;
152
+ /**
153
+ * Every move the host has made to a list's rows, oldest first, each
154
+ * numbered above the one before it (`RowMove`).
155
+ *
156
+ * A list keys each row's controls by an identity of its own, so that a row
157
+ * keeps them (a caret, a dropped file's preview, a dismissed failure) when
158
+ * an earlier row goes. It moves those identities itself for a removal it
159
+ * writes through `onChange`; for everything else a host does to the rows it
160
+ * would see only a new list, and match rows by position. Given this, a list
161
+ * replays every move made to it since it last rendered, the ones made to an
162
+ * outer list's rows included, which rename it, and moves no identity of its
163
+ * own accord: a host giving it records every move it makes here, a removal
164
+ * handed to `onRemoveItem` included. `useMethodForm`'s `rowMoves` is this
165
+ * log. See docs/upload-seam.md, "What the form keeps by id moves with the
166
+ * rows it moves".
167
+ */
168
+ rowMoves?: readonly RowMove[];
169
+ /** Ids currently mid-upload. */
170
+ uploadingIds?: ReadonlySet<string>;
171
+ /**
172
+ * Why an upload failed, keyed by the same id `onDropFile` was handed, so the
173
+ * message shows on the field that took the file rather than under the whole
174
+ * form, where only its wording could say which input failed.
175
+ *
176
+ * The map is the host's truth about each upload attempt: a host removes an
177
+ * entry when the next file is dropped at that id, and sets it again if that
178
+ * upload fails too. A clear or a typed link on the field hides the message
179
+ * until the host sends a different one or removes it, and a list holds back
180
+ * the entries a row removal left at a renumbered position, unless the host
181
+ * re-keys them itself through `onRemoveItem`. A host should
182
+ * also remove an entry once another file, or none, is written at its id, so
183
+ * that a control mounted there later shows no failure under it:
184
+ * `useMethodForm` does this for every write of its values.
185
+ */
186
+ uploadErrors?: ReadonlyMap<string, string>;
187
+ /** Resolve a `pipelex-storage://` URI to a browser-viewable URL (for previews
188
+ * of already-stored files). */
189
+ resolveUrl?: (uri: string) => Promise<string | null>;
190
+ /**
191
+ * Where a file field can take a file the host already stores. Its presence
192
+ * is the capability, as `onDropFile`'s is: every empty file field then
193
+ * offers "Choose from your team's files", and a list of files offers it for
194
+ * several at once. The choice is written as `{ url, filename }`, exactly as
195
+ * an upload's result is. See docs/sources.md.
196
+ */
197
+ fileSource?: FileSource;
198
+ }
199
+ interface FieldRendererProps {
200
+ field: RunField;
201
+ value: unknown;
202
+ onChange: (value: unknown) => void;
203
+ /** Unique, stable id used for label linkage and upload tracking. */
204
+ id: string;
205
+ error?: string;
206
+ env?: FieldEnv;
207
+ }
208
+ /**
209
+ * The single dispatch point: a normalized `RunField` → the matching control.
210
+ * Object and list fields recurse back through here, so the whole form is one
211
+ * data-driven tree with no per-type branching anywhere else.
212
+ */
213
+ declare function FieldRenderer({ field, value, onChange, id, error, env }: FieldRendererProps): react.JSX.Element;
214
+
215
+ export { DocumentField as D, type FieldEnv as F, ImageField as I, type RowMove as R, type UploadedFile as U, FieldRenderer as a, type FieldRendererProps as b, type FileValue as c };
@@ -9,7 +9,7 @@ import { Components, Renderer } from '@json-render/react';
9
9
  import { B as BrandManifest } from '../manifest-DUeAWeqa.js';
10
10
  export { b as brandManifestSchema } from '../manifest-DUeAWeqa.js';
11
11
  import * as zod_v4_core from 'zod/v4/core';
12
- import { F as FieldEnv } from '../field-renderer-B3g16qZT.js';
12
+ import { F as FieldEnv } from '../field-renderer-lNhtw-Cz.js';
13
13
  export { seedInputs } from '../core/seed.js';
14
14
  import '../core/contracts.js';
15
15
  import 'mthds/protocol';
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { cn, fieldControlClass, ResultRoot, ResultField, FieldRenderer, ToggleGroup, ToggleGroupItem, Switch, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../chunk-CSLDIWB6.js';
2
+ import { cn, brandInkClass, brandFillClass, brandFillInkClass, fieldControlClass, ResultRoot, ResultField, FieldRenderer, ToggleGroup, ToggleGroupItem, brandFillStrongClass, Switch, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../chunk-5IZWP2X4.js';
3
3
  import '../chunk-ICUEHNEH.js';
4
4
  export { seedInputs } from '../chunk-VH5CDQDR.js';
5
5
  import '../chunk-EQJBM3NS.js';
@@ -2473,7 +2473,7 @@ function Steps({ props, children }) {
2473
2473
  {
2474
2474
  className: cn(
2475
2475
  "flex h-6 w-6 items-center justify-center rounded-full text-[12px] font-semibold tabular-nums",
2476
- position < index ? "bg-primary/15 text-primary" : position === index ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"
2476
+ position < index ? cn(brandFillStrongClass, brandFillInkClass) : position === index ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"
2477
2477
  ),
2478
2478
  children: position + 1
2479
2479
  }
@@ -2744,13 +2744,29 @@ function AppBar({ props }) {
2744
2744
  },
2745
2745
  link
2746
2746
  )) }) : null,
2747
- props.tag ? /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate rounded-lg border border-primary/40 bg-primary/10 px-2.5 py-1 font-mono text-[11px] text-primary", children: props.tag }) : null
2747
+ props.tag ? /* @__PURE__ */ jsx(
2748
+ "span",
2749
+ {
2750
+ className: cn(
2751
+ "min-w-0 truncate rounded-lg border border-primary/40 px-2.5 py-1 font-mono text-[11px]",
2752
+ brandFillClass,
2753
+ brandFillInkClass
2754
+ ),
2755
+ children: props.tag
2756
+ }
2757
+ ) : null
2748
2758
  ] })
2749
2759
  ] }) });
2750
2760
  }
2751
2761
  function Hero({ props }) {
2752
2762
  return /* @__PURE__ */ jsxs("section", { className: "pt-12 pb-10 sm:pt-14 sm:pb-12", children: [
2753
- props.eyebrow ? /* @__PURE__ */ jsx("p", { className: "mb-4 text-[11px] font-medium tracking-[0.15em] text-primary uppercase", children: props.eyebrow }) : null,
2763
+ props.eyebrow ? /* @__PURE__ */ jsx(
2764
+ "p",
2765
+ {
2766
+ className: cn("mb-4 text-[11px] font-medium tracking-[0.15em] uppercase", brandInkClass),
2767
+ children: props.eyebrow
2768
+ }
2769
+ ) : null,
2754
2770
  /* @__PURE__ */ jsx("h1", { className: "max-w-2xl text-4xl font-bold tracking-tight text-foreground sm:text-[44px] sm:leading-[1.15]", children: props.headline }),
2755
2771
  props.lede ? /* @__PURE__ */ jsx("p", { className: "mt-4 max-w-2xl text-[17px] leading-relaxed text-muted-foreground", children: props.lede }) : null
2756
2772
  ] });
@@ -2797,7 +2813,7 @@ function Section({ props, children }) {
2797
2813
  return /* @__PURE__ */ jsxs("section", { className: "border-t border-border py-9 first:border-t-0 first:pt-0", children: [
2798
2814
  /* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
2799
2815
  /* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-3", children: [
2800
- props.number ? /* @__PURE__ */ jsx("span", { className: "font-mono text-[12px] text-primary", children: props.number }) : null,
2816
+ props.number ? /* @__PURE__ */ jsx("span", { className: cn("font-mono text-[12px]", brandInkClass), children: props.number }) : null,
2801
2817
  /* @__PURE__ */ jsx("h2", { className: "text-xl font-semibold tracking-tight text-foreground", children: props.title })
2802
2818
  ] }),
2803
2819
  props.lede ? /* @__PURE__ */ jsx("p", { className: "mt-1.5 text-sm text-muted-foreground", children: props.lede }) : null