@object-ui/components 17.5.0 → 17.6.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +1405 -0
  2. package/README.md +15 -7
  3. package/dist/custom/button-group.d.ts +1 -1
  4. package/dist/custom/character-count.d.ts +56 -0
  5. package/dist/custom/config-field-renderer.d.ts +1 -1
  6. package/dist/custom/config-panel-renderer.d.ts +3 -3
  7. package/dist/custom/export-progress-dialog.d.ts +1 -1
  8. package/dist/custom/filter-builder.d.ts +284 -4
  9. package/dist/custom/fullscreen-editor.d.ts +48 -2
  10. package/dist/custom/index.d.ts +28 -28
  11. package/dist/custom/item.d.ts +1 -1
  12. package/dist/custom/resizable.d.ts +1 -1
  13. package/dist/debug/index.d.ts +2 -2
  14. package/dist/index.css +22 -6
  15. package/dist/index.d.ts +21 -21
  16. package/dist/index.js +3958 -3650
  17. package/dist/index.umd.cjs +10 -6
  18. package/dist/lib/form-control-dom-props.d.ts +77 -0
  19. package/dist/lib/slider-thumb.d.ts +86 -0
  20. package/dist/notifications/index.d.ts +5 -5
  21. package/dist/renderers/basic/elements.d.ts +3 -3
  22. package/dist/renderers/basic/html-elements.d.ts +1 -1
  23. package/dist/renderers/form/validationRuleKeys.d.ts +45 -0
  24. package/dist/renderers/layout/react-page.d.ts +12 -2
  25. package/dist/share/index.d.ts +2 -2
  26. package/dist/ui/calendar.d.ts +1 -1
  27. package/dist/ui/carousel.d.ts +2 -2
  28. package/dist/ui/command.d.ts +2 -2
  29. package/dist/ui/form.d.ts +1 -1
  30. package/dist/ui/index.d.ts +47 -47
  31. package/dist/ui/pagination.d.ts +1 -1
  32. package/dist/ui/sidebar.d.ts +2 -2
  33. package/dist/ui/slider.d.ts +2 -1
  34. package/package.json +12 -12
  35. package/dist/custom/action-param-dialog.d.ts +0 -21
  36. package/dist/renderers/data-display/table.d.ts +0 -2
package/README.md CHANGED
@@ -70,20 +70,24 @@ entry goes on generating the classes your own source uses, as it always did.
70
70
  ### 2. Register Components
71
71
 
72
72
  ```tsx
73
- import { registerDefaultRenderers } from '@object-ui/components'
73
+ import { initializeComponents } from '@object-ui/components'
74
74
 
75
- registerDefaultRenderers()
75
+ initializeComponents()
76
76
  ```
77
77
 
78
+ Importing the package already registers its components as a side effect;
79
+ `initializeComponents()` is the explicit call for bundlers that would otherwise
80
+ tree-shake that import away.
81
+
78
82
  ## Usage
79
83
 
80
84
  ### With SchemaRenderer
81
85
 
82
86
  ```tsx
83
87
  import { SchemaRenderer } from '@object-ui/react'
84
- import { registerDefaultRenderers } from '@object-ui/components'
88
+ import { initializeComponents } from '@object-ui/components'
85
89
 
86
- registerDefaultRenderers()
90
+ initializeComponents()
87
91
 
88
92
  const schema = {
89
93
  type: 'card',
@@ -200,16 +204,20 @@ All components accept `className` for Tailwind classes:
200
204
  Register your own components:
201
205
 
202
206
  ```tsx
203
- import { registerRenderer } from '@object-ui/react'
207
+ import { ComponentRegistry } from '@object-ui/core'
204
208
  import { Button } from '@object-ui/components'
205
209
 
206
- function CustomButton(props) {
210
+ function CustomButton(props: Record<string, unknown>) {
207
211
  return <Button {...props} className="my-custom-style" />
208
212
  }
209
213
 
210
- registerRenderer('custom-button', CustomButton)
214
+ ComponentRegistry.register('custom-button', CustomButton)
211
215
  ```
212
216
 
217
+ `ComponentRegistry` is a process-level singleton exported by `@object-ui/core`;
218
+ `SchemaRenderer` resolves every `type` against it, so a component registered
219
+ here is renderable from schema anywhere in the app.
220
+
213
221
  ## API Reference
214
222
 
215
223
  See [full documentation](https://objectui.org/docs/components) for detailed API reference.
@@ -1,5 +1,5 @@
1
1
  import { VariantProps } from 'class-variance-authority';
2
- import { Separator } from '../ui/separator';
2
+ import { Separator } from '../ui/separator.js';
3
3
  declare const buttonGroupVariants: (props?: ({
4
4
  orientation?: "horizontal" | "vertical" | null | undefined;
5
5
  } & import('class-variance-authority/types').ClassProp) | undefined) => string;
@@ -0,0 +1,56 @@
1
+ import { default as React } from 'react';
2
+ export interface CharacterCountProps {
3
+ /**
4
+ * Length of the text this surface is counting. The inline surface passes the
5
+ * committed value's length; the fullscreen dialog passes the DRAFT's, which
6
+ * is the number that is true of what the user is looking at.
7
+ */
8
+ length: number;
9
+ /**
10
+ * The declared cap. Callers render this component only when the field
11
+ * declares one, so it is non-optional here — a counter with no cap to count
12
+ * against has nothing to say and must not be mounted at all (an unconditional
13
+ * live region in every long-text field is the objectui#3408 defect back
14
+ * again).
15
+ */
16
+ maxLength: number;
17
+ /**
18
+ * The description node's `id`, minted by the host from `useId()`.
19
+ *
20
+ * The host MUST also name it in the edited control's `aria-describedby` —
21
+ * this component renders the sentence, only the host knows which element is
22
+ * being described, and a description nothing references is a node no screen
23
+ * reader will ever reach. That association is asserted from the OUTSIDE (the
24
+ * tests resolve `aria-describedby` and concatenate the referenced nodes' text,
25
+ * exactly as an assistive technology does) rather than trusted here.
26
+ *
27
+ * Two surfaces of one widget therefore need two ids: the dialog's draft and
28
+ * the inline value diverge the moment the user types in the dialog, so one
29
+ * shared id would point both controls at whichever sentence rendered last.
30
+ */
31
+ descriptionId: string;
32
+ /**
33
+ * Render node 3, the threshold-gated debounced `aria-live` status region.
34
+ *
35
+ * Required on purpose — see the component doc above. `false` removes the
36
+ * region from the DOM entirely (not merely empties it), which is the only
37
+ * form of "no live region" a test can tell apart from "a live region that
38
+ * happens to be silent right now".
39
+ */
40
+ announceNearLimit: boolean;
41
+ /**
42
+ * Classes for the visible digits. Required because placement is entirely the
43
+ * surface's business — inline pins them to the textarea's bottom-right
44
+ * corner, the dialog lays them out as a footer item — and a default borrowed
45
+ * from either one would position the other wrongly while looking deliberate.
46
+ */
47
+ className: string;
48
+ /**
49
+ * `data-testid` for the visible digits, namespaced per surface
50
+ * (`textarea-character-count`, `textarea-fullscreen-character-count`) so a
51
+ * test that finds digits can say WHICH surface it found them on. Same
52
+ * convention, same reason, as `FullscreenFieldEditor`'s `testIdPrefix`.
53
+ */
54
+ testId: string;
55
+ }
56
+ export declare function CharacterCount({ length, maxLength, descriptionId, announceNearLimit, className, testId, }: CharacterCountProps): React.JSX.Element;
@@ -1,4 +1,4 @@
1
- import { ConfigField } from '../types/config-panel';
1
+ import { ConfigField } from '../types/config-panel.js';
2
2
  /**
3
3
  * ObjectUI
4
4
  * Copyright (c) 2024-present ObjectStack Inc.
@@ -1,4 +1,4 @@
1
- import { ConfigPanelSchema } from '../types/config-panel';
1
+ import { ConfigPanelSchema } from '../types/config-panel.js';
2
2
  /**
3
3
  * ObjectUI
4
4
  * Copyright (c) 2024-present ObjectStack Inc.
@@ -32,9 +32,9 @@ export interface ConfigPanelRendererProps {
32
32
  className?: string;
33
33
  /** Additional inline styles applied to the panel root (e.g. to override `--config-panel-width`). */
34
34
  style?: React.CSSProperties;
35
- /** Label for save button (default: "Save") */
35
+ /** Label for save button (default: the locale pack's `common.save`) */
36
36
  saveLabel?: string;
37
- /** Label for discard button (default: "Discard") */
37
+ /** Label for discard button (default: the locale pack's `common.discard`) */
38
38
  discardLabel?: string;
39
39
  /** Ref for the panel root element */
40
40
  panelRef?: React.Ref<HTMLDivElement>;
@@ -1,4 +1,4 @@
1
- import { UseExportJobReturn } from '../hooks/use-export-job';
1
+ import { UseExportJobReturn } from '../hooks/use-export-job.js';
2
2
  /**
3
3
  * ObjectUI
4
4
  * Copyright (c) 2024-present ObjectStack Inc.
@@ -42,7 +42,7 @@ export interface FilterBuilderProps {
42
42
  value: string;
43
43
  label: string;
44
44
  }>;
45
- /** For lookup/master_detail/user/owner fields — referenced object name */
45
+ /** For lookup/master_detail/user fields — referenced object name */
46
46
  referenceTo?: string;
47
47
  /** Display field on the referenced object (defaults to "name") */
48
48
  displayField?: string;
@@ -53,6 +53,17 @@ export interface FilterBuilderProps {
53
53
  onChange?: (value: FilterGroup) => void;
54
54
  className?: string;
55
55
  showClearAll?: boolean;
56
+ /**
57
+ * Opt-in operator ids ({@link OPT_IN_OPERATORS}) this instance may offer, on
58
+ * top of the ones every FilterBuilder draws.
59
+ *
60
+ * The dropdown is shared by consumers that persist a filter into DIFFERENT
61
+ * dialects, and an operator one dialect can carry is not automatically
62
+ * storable in another — so whether an opt-in operator is offered is the
63
+ * consumer's answer, not this component's. See {@link OPT_IN_OPERATORS} for
64
+ * the measurement that made this a prop instead of one global list.
65
+ */
66
+ extraOperators?: readonly string[];
56
67
  }
57
68
  declare const defaultOperators: readonly [{
58
69
  readonly value: "equals";
@@ -63,6 +74,9 @@ declare const defaultOperators: readonly [{
63
74
  }, {
64
75
  readonly value: "contains";
65
76
  readonly label: "Contains";
77
+ }, {
78
+ readonly value: "containsCaseInsensitive";
79
+ readonly label: "Contains (ignore case)";
66
80
  }, {
67
81
  readonly value: "notContains";
68
82
  readonly label: "Does not contain";
@@ -120,7 +134,8 @@ declare const defaultOperators: readonly [{
120
134
  }];
121
135
  /**
122
136
  * The FilterBuilder's own operator vocabulary — every id its dropdown can
123
- * hold, derived from the operators it actually renders.
137
+ * hold, derived from the operators it actually renders. Includes the opt-in
138
+ * ids: they are drawable, just not offered unconditionally.
124
139
  *
125
140
  * Exported because several translation tables map an external vocabulary
126
141
  * (the spec's `VIEW_FILTER_OPERATORS`, Mongo `$`-tokens) *onto* this one, and
@@ -129,10 +144,275 @@ declare const defaultOperators: readonly [{
129
144
  * rather than restating it, so adding an operator here is the only edit
130
145
  * needed to widen them.
131
146
  */
132
- export declare const FILTER_BUILDER_OPERATORS: ("endsWith" | "startsWith" | "in" | "after" | "equals" | "notEquals" | "contains" | "notContains" | "isEmpty" | "isNotEmpty" | "greaterThan" | "lessThan" | "greaterOrEqual" | "lessOrEqual" | "before" | "between" | "notIn" | "isNull" | "isNotNull" | "exists" | "notExists")[];
147
+ export declare const FILTER_BUILDER_OPERATORS: ("endsWith" | "startsWith" | "in" | "after" | "equals" | "notEquals" | "contains" | "containsCaseInsensitive" | "notContains" | "isEmpty" | "isNotEmpty" | "greaterThan" | "lessThan" | "greaterOrEqual" | "lessOrEqual" | "before" | "between" | "notIn" | "isNull" | "isNotNull" | "exists" | "notExists")[];
133
148
  /** An operator id the FilterBuilder can render. */
134
149
  export type FilterBuilderOperator = (typeof defaultOperators)[number]['value'];
135
- declare function FilterBuilder({ fields, value, onChange, className, showClearAll, }: FilterBuilderProps): React.JSX.Element;
150
+ /**
151
+ * Operator ids for which this builder renders NO value input — so "no value"
152
+ * is the row's FINISHED state, not an unfinished one (objectui#4744).
153
+ *
154
+ * This is the source of truth for that distinction, and it lives here because
155
+ * this component is the thing that decides it: `needsValueInput` below is
156
+ * defined as the complement of this set, so the two cannot say different
157
+ * things. Every consumer that has to tell a complete value-less row from a
158
+ * half-filled one reads it FROM here rather than restating it:
159
+ *
160
+ * - `plugin-list`'s `convertFilterGroupToAST` — what the live grid QUERIES;
161
+ * - `app-shell`'s `foldFilterGroupToSpecRules` — what a saved view PERSISTS
162
+ * (its `VALUELESS_FILTER_OPERATORS` is this set plus the canonical spec
163
+ * spellings, which only that layer sees).
164
+ *
165
+ * Placement is forced by the dependency graph — `app-shell` depends on
166
+ * `plugin-list` depends on this package, so this is the only module all three
167
+ * can reach — and it is also where the set belongs on the merits: the fact it
168
+ * states is a fact about what this dropdown draws.
169
+ *
170
+ * Why it exists at all: each consumer used to keep its own copy, and the
171
+ * live-grid copy listed only `isEmpty`/`isNotEmpty`. A fresh row is seeded
172
+ * `{ operator: 'equals', value: '' }` and the operator dropdown preserves
173
+ * `value`, so picking **Is null** as the first action left `value: ''` — the
174
+ * grid read that as an unfinished row, dropped it, and applied NO filter at
175
+ * all while the panel showed one. Silent, and every record came back.
176
+ *
177
+ * `exists` / `notExists` stay listed even though `OPT_IN_OPERATORS` withholds
178
+ * them from most consumers (objectui#4736): the builder still draws them
179
+ * value-less for `FilterConditionField`, which is exactly what this set
180
+ * describes. Whether a consumer can EXPRESS an operator is a separate
181
+ * question, answered by `OPT_IN_OPERATORS` and by each consumer's parity test.
182
+ */
183
+ export declare const VALUELESS_FILTER_BUILDER_OPERATORS: ReadonlySet<string>;
184
+ /**
185
+ * The SHAPE an operator's `value` must have — the question
186
+ * `ViewFilterRuleSchema` asks, asked here in the one place that decides what
187
+ * input the user is given (objectui#3958).
188
+ *
189
+ * - `list` — membership over any arity, `[]` included (`in` / `not_in`).
190
+ * - `pair` — exactly two bounds, in order (`between`).
191
+ * - `scalar` — everything else, including the value-less operators above
192
+ * (they draw no input at all, so their shape never reaches storage).
193
+ */
194
+ export type FilterValueArity = "scalar" | "list" | "pair";
195
+ /**
196
+ * Which value shape `operator` takes, in EITHER dialect.
197
+ *
198
+ * The argument is folded through the spec's own `normalizeFilterOperator`
199
+ * first, so the builder's camelCase dropdown ids (`notIn`) and the canonical
200
+ * spellings a stored rule carries (`not_in`) land on the same answer. That
201
+ * fold is the whole point: one vocabulary, declared upstream, consulted here —
202
+ * not a second local dialect that has to be kept in sync by hand.
203
+ *
204
+ * @internal exported for tests
205
+ */
206
+ export declare function filterValueArity(operator: string): FilterValueArity;
207
+ /**
208
+ * Re-shape a row's `value` for the operator it is being changed TO.
209
+ *
210
+ * Switching the operator dropdown used to update `operator` alone, so the
211
+ * value the previous family had produced simply survived the switch — a scalar
212
+ * `"acme"` (or the seed `""`) still sitting there under `in`, which is
213
+ * precisely the shape `ViewFilterRuleSchema` refuses. Re-shaping rather than
214
+ * blanking keeps what the user typed wherever it still means something:
215
+ *
216
+ * - to `list`: a scalar becomes a one-element list (`"acme"` → `["acme"]`),
217
+ * an empty scalar becomes `[]` — a real, spec-valid predicate;
218
+ * - to `pair`: the first two entries become `[min, max]`, padded with `""`;
219
+ * with nothing to carry it collapses to `[]`, which the write path reads
220
+ * as "row not filled in yet" and drops, exactly as it drops an empty
221
+ * scalar today;
222
+ * - to `scalar`: the first entry of a list survives; the rest cannot, and
223
+ * dropping them is the honest answer — `equals` compares against one value.
224
+ *
225
+ * @internal exported for tests
226
+ */
227
+ export declare function reshapeFilterValue(value: FilterBuilderCondition["value"], nextOperator: string): FilterBuilderCondition["value"];
228
+ /**
229
+ * The operator a row keeps after its FIELD is changed, given the operators the
230
+ * NEW field offers (objectui#4768).
231
+ *
232
+ * Each field type has its own operator bucket, and the buckets are not nested:
233
+ * a `select` column offers `in` / `notIn`, a `text` column does not. Changing
234
+ * the field used to write `{ field }` alone, so the row's operator survived
235
+ * into a bucket that no longer contains it — `in` on a text column. The Radix
236
+ * trigger then had nothing to render (its `SelectValue` matches against the
237
+ * `SelectItem`s actually mounted) and went BLANK, while the row went on
238
+ * filtering by an operator the user could no longer see or reach.
239
+ *
240
+ * So the two edits are one edit, exactly as `changeOperator` made the operator
241
+ * and its value shape one edit: an operator the new bucket still offers is the
242
+ * user's choice and is KEPT; one it does not is replaced by the bucket's first
243
+ * entry (`equals` for every bucket this builder draws), and the caller then
244
+ * re-shapes the value for that operator's family.
245
+ *
246
+ * Membership is decided CANONICALLY, through the spec's own
247
+ * `normalizeFilterOperator` — the same fold `filterValueArity` uses. A stored
248
+ * rule can reach this builder spelled `not_in` while the dropdown lists the
249
+ * alias `notIn`; those are one operator, so a select→lookup switch must not
250
+ * silently rewrite the author's operator to `equals` just because the two
251
+ * spellings differ. The fold is safe to compare through because it is
252
+ * INJECTIVE over this builder's vocabulary — all 22 ids in `defaultOperators`
253
+ * normalize to 22 distinct canonical operators, pinned in
254
+ * `filter-builder-field-switch-operator.test.tsx` — so no two OFFERED
255
+ * operators can ever collapse onto one another.
256
+ *
257
+ * @internal exported for tests
258
+ */
259
+ export declare function reconcileOperatorForField(operator: string, offeredOperators: ReadonlyArray<{
260
+ value: string;
261
+ }>): string;
262
+ /**
263
+ * Re-TYPE a row's `value` for the field it is being changed TO — the type
264
+ * question, standing beside {@link reshapeFilterValue}'s shape question
265
+ * (objectui#4781).
266
+ *
267
+ * Changing a row's field used to carry the value through untouched whenever the
268
+ * shape did not have to change, which is right about the shape and wrong about
269
+ * the type: the value input is re-drawn from the NEW field's type, and an
270
+ * `<input type="number">` renders a non-numeric value as BLANK. So a `text`
271
+ * row filtered `equals "acme"`, pointed at a number column, showed an empty box
272
+ * while the row went on carrying `"acme"` — `foldFilterGroupToSpecRules`
273
+ * persisted it and the live grid queried `amount equals "acme"`. The same
274
+ * invisible-value shape objectui#4768 closed on the operator, one column over.
275
+ *
276
+ * Carry-if-possible, exactly as `reshapeFilterValue` carries what it can across
277
+ * a family change: `"42"` moved to a number column becomes `42` and survives;
278
+ * `"acme"` cannot be read as a number by any clean reading, so it clears to the
279
+ * family's empty shape — scalar `""`, list `[]`, pair `[]`. What a value can
280
+ * become is decided in one place, {@link convertScalarToFamily}.
281
+ *
282
+ * The arity is read from the operator through the same `filterValueArity` fold
283
+ * the rest of this component uses, because each shape clears differently:
284
+ *
285
+ * - `scalar` — converted, or `""`.
286
+ * - `list` — converted ENTRY BY ENTRY, keeping the ones that carry:
287
+ * `["42", "acme"]` → `[42]`, and `[]` when none do. Reachable today only
288
+ * between the two buckets that offer `in`/`notIn` (`select` ↔ `lookup`),
289
+ * which are both text-family, so the conversion is the identity there; it
290
+ * is written for the family the operator lands in rather than for today's
291
+ * buckets, and pinned directly on this helper.
292
+ * - `pair` — both bounds converted independently, an unconvertible bound
293
+ * becoming `""` so the range keeps its two slots; both empty collapses to
294
+ * `[]`, the "not filled in yet" shape `reshapeFilterValue` also produces
295
+ * and the write path drops.
296
+ *
297
+ * @internal exported for tests
298
+ */
299
+ export declare function retypeFilterValue(value: FilterBuilderCondition["value"], fieldType: string | undefined, operator: string): FilterBuilderCondition["value"];
300
+ /** The shape of a `fields` entry that the option-domain helpers below read. */
301
+ type OptionBearingField = {
302
+ type?: string;
303
+ options?: ReadonlyArray<{
304
+ value: string;
305
+ label: string;
306
+ }>;
307
+ };
308
+ /**
309
+ * Narrow a row's `value` to the option DOMAIN of the field it is being changed
310
+ * TO — the value-domain question, standing beside {@link retypeFilterValue}'s
311
+ * type question and {@link reshapeFilterValue}'s shape question
312
+ * (objectui#4874).
313
+ *
314
+ * objectui#4781 settled the TYPE half and deliberately stopped there: `select`
315
+ * and `lookup` are text-family, so `"acme"` is a value those columns can
316
+ * perfectly well HOLD. What it cannot be is one of the column's options — and
317
+ * the control drawn for those columns is a Radix Select, whose trigger renders
318
+ * only what one of its mounted `SelectItem`s carries. So a text row filtered
319
+ * `equals "acme"`, pointed at a picklist column of `won`/`lost`, showed a BLANK
320
+ * trigger while the row went on carrying `"acme"`:
321
+ * `foldFilterGroupToSpecRules` persisted it and the live grid queried
322
+ * `stage equals "acme"`. The third face of the same invisible value —
323
+ * objectui#4768 on the operator, objectui#4781 on the value's type, this one on
324
+ * its domain.
325
+ *
326
+ * Ruled 2026-08-17 as **A + C combined**, and the A half is this function:
327
+ * clear on non-membership, but ONLY where the option set is the column's whole
328
+ * domain ({@link hasStaticOptionDomain}). A remote/async column's value is
329
+ * returned untouched — clearing it would delete a perfectly valid lookup id on
330
+ * the strength of a local page that never claimed to be complete, and the C
331
+ * half (the render path's temporary option) is what keeps it visible instead.
332
+ * Option B — keep the value and tolerate it being invisible — stays rejected,
333
+ * as it was on objectui#4781.
334
+ *
335
+ * The empty shape a cleared value falls to is objectui#4781's, unchanged and
336
+ * read from the same `filterValueArity` fold:
337
+ *
338
+ * - `scalar` — `""`.
339
+ * - `list` — filtered ENTRY BY ENTRY, keeping the members: `["acme", "won"]`
340
+ * → `["won"]`, and `[]` when none survive. Per-entry rather than
341
+ * all-or-nothing because each entry is its own filter term, and one bad
342
+ * term is no reason to throw away the user's good ones.
343
+ * - `pair` — untouched; `between` is not option-driven.
344
+ *
345
+ * @internal exported for tests
346
+ */
347
+ export declare function narrowFilterValueToOptions(value: FilterBuilderCondition["value"], field: OptionBearingField | undefined, operator: string): FilterBuilderCondition["value"];
348
+ /**
349
+ * Is this row's value FINISHED for the operator it carries — the arity-aware
350
+ * reading of "the user has filled this in" (objectstack#8815).
351
+ *
352
+ * Both write paths used to ask this with one shape-blind predicate:
353
+ *
354
+ * ```ts
355
+ * value == null || value === '' || (Array.isArray(value) && value.length === 0)
356
+ * ```
357
+ *
358
+ * which is right for `scalar` and `list` and WRONG for `pair`. A `between` row
359
+ * with one bound typed is `["2024-01-01", ""]` — an array of length 2, so it
360
+ * read as complete, and both consumers carried it through. The live grid then
361
+ * queried a range with an empty upper bound and the server refused the whole
362
+ * view (`400 INVALID_FILTER`), which is the "该视图的查询被拒绝" the reporter
363
+ * hit; the saved-view path persisted the same half-range, so the refusal came
364
+ * back on every later read of that view.
365
+ *
366
+ * The spec cannot catch this on the way past — measured against
367
+ * `ViewFilterRuleSchema` on `@objectstack/spec` 17.0.0: `["2024-01-01", ""]` is
368
+ * ACCEPTED (it counts the two slots, not what is in them), while a scalar or a
369
+ * one-element array is refused. So the empty bound sails through authoring
370
+ * validation and only dies at query time, one layer too late to be actionable.
371
+ * That makes it the PRODUCER's job to not emit it — this component is what
372
+ * decides when a row is finished, exactly as it already decides which operators
373
+ * finish with no value at all ({@link VALUELESS_FILTER_BUILDER_OPERATORS}), and
374
+ * for the same reason: each consumer keeping its own copy is how the two came to
375
+ * disagree in the first place.
376
+ *
377
+ * The arity is read through the same `filterValueArity` fold the rest of this
378
+ * file uses, so a stored rule spelled canonically gets the same answer as the
379
+ * dropdown's own id.
380
+ *
381
+ * What each shape needs:
382
+ *
383
+ * - `pair` — BOTH bounds present. A range missing an end is not a narrower
384
+ * range, it is a query the server refuses.
385
+ * - `list` — at least one entry; `[]` is the "nothing chosen yet" shape.
386
+ * - `scalar` — anything that is not the unfilled shape.
387
+ *
388
+ * "Present" is {@link isValueUnset}, never `!bound` (objectui#4873): `0` is a
389
+ * real bound on a number column and `false` is a real value, and reading either
390
+ * as unfilled would drop a filter the user can see on screen.
391
+ *
392
+ * This answers the VALUE question only. Whether an operator wants a value at all
393
+ * stays with each caller, because the two layers see different vocabularies:
394
+ * `app-shell` also has to recognise the canonical spellings (`is_null`) that
395
+ * never reach this dropdown.
396
+ *
397
+ * @internal exported for tests
398
+ */
399
+ export declare function isFilterValueComplete(operator: string, value: FilterBuilderCondition["value"] | undefined | null): boolean;
400
+ /**
401
+ * The operators the dropdown offers for a field of `fieldType`, given the
402
+ * opt-in ids this instance was granted.
403
+ *
404
+ * A pure function rather than a closure so the selection rule — and above all
405
+ * the {@link OPT_IN_OPERATORS} gate, whose whole job is to keep an operator OFF
406
+ * the surfaces that cannot store it — is assertable without driving a Radix
407
+ * popover open in jsdom.
408
+ *
409
+ * @internal exported for tests
410
+ */
411
+ export declare function operatorsForFieldType(fieldType: string | undefined, extraOperators?: readonly string[]): ReadonlyArray<{
412
+ value: string;
413
+ label: string;
414
+ }>;
415
+ declare function FilterBuilder({ fields, value, onChange, className, showClearAll, extraOperators, }: FilterBuilderProps): React.JSX.Element;
136
416
  declare namespace FilterBuilder {
137
417
  var displayName: string;
138
418
  }
@@ -1,4 +1,29 @@
1
1
  import { default as React } from 'react';
2
+ /**
3
+ * The accessibility attributes the dialog's injected editor MUST carry, handed
4
+ * to `children` as its required fourth argument (objectui#4824 / #4832).
5
+ *
6
+ * Shaped as a spreadable set of real DOM attributes rather than the ids behind
7
+ * them, on purpose: `{...aria}` is the whole integration, so a host cannot get
8
+ * the composition subtly wrong, cannot name a node outside the dialog, and
9
+ * cannot compute its own `aria-invalid` from a prop it forgot to plumb — the
10
+ * three ways every one of the three surfaces got this wrong before.
11
+ */
12
+ export interface FullscreenEditorAria {
13
+ /**
14
+ * The dialog title's text node — the field label (#3393) reused as the
15
+ * control's accessible name rather than a second, string-valued author for it.
16
+ */
17
+ 'aria-labelledby': string;
18
+ /** Mirrors `error`, so the dialog cannot disagree with the inline control. */
19
+ 'aria-invalid': boolean;
20
+ /**
21
+ * The dialog-local message node. A SINGLE id, present only when `error` is —
22
+ * `aria-errormessage` is exposed only alongside `aria-invalid="true"`, and
23
+ * this repo's toolchain does not accept an id list here.
24
+ */
25
+ 'aria-errormessage'?: string;
26
+ }
2
27
  export interface FullscreenEditorProps {
3
28
  /**
4
29
  * The committed value. Seeds the draft each time the dialog opens — read at
@@ -39,6 +64,22 @@ export interface FullscreenEditorProps {
39
64
  * widgets and applies it on the built-in branch.
40
65
  */
41
66
  disabled?: boolean;
67
+ /**
68
+ * The field's validation message, or `undefined` when the field is valid.
69
+ *
70
+ * **Required, not optional** (objectui#4824). Every consumer already has it —
71
+ * the registered widgets take `error` off the widget props contract
72
+ * (objectui#3222), the built-in branch reads `fieldState.error?.message` — and
73
+ * an omitted `error` reproduces this issue exactly: a dialog that announces
74
+ * `aria-invalid="false"` while the same field's inline control announces
75
+ * `true`. A key the type system makes every call site answer cannot be
76
+ * forgotten by the next surface; an optional one was, three times.
77
+ *
78
+ * **Producers**: `TextAreaField`, `RichTextField`, and the form renderer's
79
+ * built-in `textarea` branch (which reads it off the PRE-strip props, exactly
80
+ * like `label` — `stripRendererOnlyProps` discards `error` for the DOM).
81
+ */
82
+ error: string | undefined;
42
83
  /**
43
84
  * The editor itself, rendered inside the dialog body against the draft. The
44
85
  * host passes the SAME editor it renders inline.
@@ -48,9 +89,14 @@ export interface FullscreenEditorProps {
48
89
  * editor's disabled state belongs on. Ignoring it is not a write-back hole —
49
90
  * `onCommit` is gated here regardless — but it does leave a control that looks
50
91
  * editable while the field is not, so every in-repo host applies it.
92
+ *
93
+ * The fourth argument is the accessibility set this component owns — name and
94
+ * validation state — and it is spread onto the control the host renders. See
95
+ * `FullscreenEditorAria` and the section above for why it is computed here
96
+ * rather than by each host.
51
97
  */
52
- children: (draft: string, setDraft: (next: string) => void, disabled: boolean) => React.ReactNode;
98
+ children: (draft: string, setDraft: (next: string) => void, disabled: boolean, aria: FullscreenEditorAria) => React.ReactNode;
53
99
  /** Optional footer status for the draft (e.g. a character counter). */
54
100
  footer?: (draft: string) => React.ReactNode;
55
101
  }
56
- export declare function FullscreenEditor({ value, onCommit, label, testIdPrefix, readOnly, disabled, children, footer, }: FullscreenEditorProps): React.JSX.Element | null;
102
+ export declare function FullscreenEditor({ value, onCommit, label, testIdPrefix, readOnly, disabled, error, children, footer, }: FullscreenEditorProps): React.JSX.Element | null;
@@ -1,28 +1,28 @@
1
- export * from './button-group';
2
- export * from './combobox';
3
- export * from './config-row';
4
- export * from './config-field-renderer';
5
- export * from './config-panel-renderer';
6
- export * from './date-picker';
7
- export * from './empty';
8
- export * from './field';
9
- export * from './filter-builder';
10
- export * from './fullscreen-editor';
11
- export * from './input-group';
12
- export * from './item';
13
- export * from './kbd';
14
- export * from './native-select';
15
- export * from './navigation-overlay';
16
- export * from './resizable';
17
- export * from './section-header';
18
- export * from './spinner';
19
- export * from './sort-builder';
20
- export * from './grouping-editor';
21
- export * from './action-param-dialog';
22
- export * from './view-skeleton';
23
- export * from './shimmer-skeleton';
24
- export * from './RecordTitleChip';
25
- export * from './refresh-indicator';
26
- export * from './view-states';
27
- export * from './mobile-dialog-content';
28
- export * from './export-progress-dialog';
1
+ export * from './button-group.js';
2
+ export * from './character-count.js';
3
+ export * from './combobox.js';
4
+ export * from './config-row.js';
5
+ export * from './config-field-renderer.js';
6
+ export * from './config-panel-renderer.js';
7
+ export * from './date-picker.js';
8
+ export * from './empty.js';
9
+ export * from './field.js';
10
+ export * from './filter-builder.js';
11
+ export * from './fullscreen-editor.js';
12
+ export * from './input-group.js';
13
+ export * from './item.js';
14
+ export * from './kbd.js';
15
+ export * from './native-select.js';
16
+ export * from './navigation-overlay.js';
17
+ export * from './resizable.js';
18
+ export * from './section-header.js';
19
+ export * from './spinner.js';
20
+ export * from './sort-builder.js';
21
+ export * from './grouping-editor.js';
22
+ export * from './view-skeleton.js';
23
+ export * from './shimmer-skeleton.js';
24
+ export * from './RecordTitleChip.js';
25
+ export * from './refresh-indicator.js';
26
+ export * from './view-states.js';
27
+ export * from './mobile-dialog-content.js';
28
+ export * from './export-progress-dialog.js';
@@ -1,5 +1,5 @@
1
1
  import { VariantProps } from 'class-variance-authority';
2
- import { Separator } from '../ui/separator';
2
+ import { Separator } from '../ui/separator.js';
3
3
  /**
4
4
  * ObjectUI
5
5
  * Copyright (c) 2024-present ObjectStack Inc.
@@ -1,4 +1,4 @@
1
- import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../ui/resizable';
1
+ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../ui/resizable.js';
2
2
  /**
3
3
  * ObjectUI
4
4
  * Copyright (c) 2024-present ObjectStack Inc.
@@ -5,5 +5,5 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- export { DebugPanel } from './DebugPanel';
9
- export type { DebugPanelProps, DebugPanelTab } from './DebugPanel';
8
+ export { DebugPanel } from './DebugPanel.js';
9
+ export type { DebugPanelProps, DebugPanelTab } from './DebugPanel.js';