@danxbot/ui 11.0.0 → 12.0.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,30 @@
1
+ import { type ReactNode } from "react";
2
+ import { type EditActivation } from "./edit/activation";
3
+ interface EditGutterFieldProps {
4
+ /**
5
+ * The pencil's accessible name — say WHAT it edits ("Edit title"), because
6
+ * a screen reader lists it among every other pencil on the page.
7
+ */
8
+ label: string;
9
+ /**
10
+ * Render the field, spreading the activation onto it:
11
+ * `{(edit) => <InlineEdit {...edit} … />}`.
12
+ */
13
+ children: (activation: EditActivation) => ReactNode;
14
+ /** Render no pencil — the field cannot be edited here. */
15
+ disabled?: boolean;
16
+ className?: string;
17
+ }
18
+ /**
19
+ * The gutter keeps `editing` itself (omit both), or the caller owns it: pass
20
+ * `editing` AND `onEditingChange` (`EditActivation`) — the pencil then asks
21
+ * through `onEditingChange(true)` and the field's close arrives through
22
+ * `onEditingChange(false)`. One without the other is a type error and throws
23
+ * at render, for the reason `assertActivationPair` gives.
24
+ */
25
+ export type EditGutterProps = EditGutterFieldProps & ({
26
+ editing?: undefined;
27
+ onEditingChange?: (editing: boolean) => void;
28
+ } | EditActivation);
29
+ export declare function EditGutter({ label, children, editing: editingProp, onEditingChange, disabled, className }: EditGutterProps): import("react").JSX.Element;
30
+ export {};
@@ -1,23 +1,19 @@
1
1
  import { type Ref } from "react";
2
2
  import { useRender } from "@base-ui/react/use-render";
3
- /**
4
- * The caller's refusal of a value, with the reason that will be shown.
5
- * Unchanged from 5.2.1 — see `InlineEditOutcome`.
6
- */
7
- export interface InlineEditRefusal {
8
- refusal: string;
9
- }
10
- /** What `onCommit` may report. Returning nothing accepts the value. */
11
- export type InlineEditOutcome = void | InlineEditRefusal;
12
- export interface InlineEditProps {
3
+ import { type EditActivation } from "./edit/activation";
4
+ import { type EditCommit } from "./edit/outcome";
5
+ interface InlineEditFieldProps {
13
6
  /**
14
7
  * The committed value. Controlled — this component never holds the truth,
15
8
  * only the draft while somebody is typing one.
16
9
  */
17
10
  value: string;
18
11
  /**
19
- * Commit a new value. Called ONLY when the live text actually differs from
20
- * `value`.
12
+ * Commit a new value. Called ONLY when the live text differs both from
13
+ * `value` and from the value editing began from — text the reader did not
14
+ * change is never written back, even if `value` moved underneath the open
15
+ * field (with `autosaveMs`, "unchanged" is measured against the last value
16
+ * this edit committed instead — see "AUTOSAVE").
21
17
  *
22
18
  * `base` is the `value` this control saw at the moment editing BEGAN —
23
19
  * captured once and handed back unchanged regardless of how long the edit or
@@ -30,7 +26,7 @@ export interface InlineEditProps {
30
26
  * it settles. Resolve/return `{ refusal }` to reject the value with a
31
27
  * reason; throw or reject to report that the save itself failed.
32
28
  */
33
- onCommit: (value: string, base: string) => InlineEditOutcome | Promise<InlineEditOutcome>;
29
+ onCommit: EditCommit;
34
30
  /**
35
31
  * The refusals that are knowable WITHOUT a request. Return the reason, or
36
32
  * `null` to accept. Runs after the unchanged check and before `onCommit`.
@@ -100,7 +96,10 @@ export interface InlineEditProps {
100
96
  /**
101
97
  * This value cannot be edited at all, AND WHY. `{ reason }`, matching
102
98
  * `SettingRow.readOnly` — a control that refuses in silence is
103
- * indistinguishable from one that is broken.
99
+ * indistinguishable from one that is broken. Under controlled activation a
100
+ * request to open (`editing` true) is declined by handing the closed state
101
+ * straight back through `onEditingChange(false)`, so a pencil driving it
102
+ * returns instead of waiting on a field that will never open (DX-3372 H3).
104
103
  */
105
104
  readOnly?: {
106
105
  reason: string;
@@ -109,18 +108,13 @@ export interface InlineEditProps {
109
108
  * What is shown when the commit REJECTS (a thrown error or a rejected
110
109
  * promise — never a `{ refusal }`, which renders its own text).
111
110
  *
112
- * @default "Could not save. Your text is still here — press Enter to try again."
111
+ * @default "Could not save. Your text is still here — press <key> to try again.",
112
+ * where <key> is the key that commits in this configuration: Enter, or
113
+ * Ctrl+Enter for a `multiline` field without `autosaveMs`.
113
114
  */
114
115
  failureMessage?: string;
115
116
  /** The rejected error, handed on so it can be logged or reported. */
116
117
  onError?: (error: unknown) => void;
117
- /**
118
- * Fires exactly when editing starts and exactly when it ends — never while a
119
- * read-only control merely declines to open. Use it to suspend any
120
- * re-render-from-server logic of your own for as long as someone is
121
- * actually looking at the field; see the module comment.
122
- */
123
- onEditingChange?: (editing: boolean) => void;
124
118
  /** Render as a different element — see `Button`'s own `render` for the shape. */
125
119
  render?: useRender.RenderProp;
126
120
  /**
@@ -135,4 +129,31 @@ export interface InlineEditProps {
135
129
  messageClassName?: string;
136
130
  ref?: Ref<HTMLElement>;
137
131
  }
138
- export declare function InlineEdit({ value, onCommit, validate, liveValidate, label, placeholder, size, multiline, autosaveMs, readOnly, failureMessage, onError, onEditingChange, render, className, messageClassName, inputMode, ref, }: InlineEditProps): import("react").JSX.Element;
132
+ /**
133
+ * SELF-ACTIVATED: omit `editing` and the control opens itself on click, Enter
134
+ * or F2. `onEditingChange` then only observes — it fires when the control
135
+ * starts editing and when it stops, so a caller can suspend its own
136
+ * re-render-from-server logic for exactly as long as someone is typing.
137
+ */
138
+ interface InlineEditSelfActivated {
139
+ editing?: undefined;
140
+ onEditingChange?: (editing: boolean) => void;
141
+ }
142
+ /**
143
+ * The props. `editing` + `onEditingChange` are a discriminated pair
144
+ * (DX-3372 review P0): either omit `editing` (self-activated, above) or pass
145
+ * BOTH (`EditActivation` — controlled). Controlled, the control never opens
146
+ * itself; `true` opens it focused with the caret at the end; it hands the
147
+ * closed state back through `onEditingChange(false)` after a landed commit,
148
+ * Escape, or a refused opening (read-only). Setting `editing` false yourself
149
+ * ends the edit exactly as Escape does — nothing more is written. A save that
150
+ * is already out when you do cannot be recalled: it still reaches your
151
+ * `onCommit` promise, but its outcome no longer touches the field — no
152
+ * message, no focus move, no second `onEditingChange(false)` — and a commit
153
+ * from the next opening waits for it before starting (see
154
+ * `edit/use-edit-session.ts`). At rest a controlled control leaves the tab
155
+ * order and drops its hover affordance, so the trigger is the one stop.
156
+ */
157
+ export type InlineEditProps = InlineEditFieldProps & (InlineEditSelfActivated | EditActivation);
158
+ export declare function InlineEdit(props: InlineEditProps): import("react").JSX.Element;
159
+ export {};
@@ -0,0 +1,55 @@
1
+ import { type ReactNode } from "react";
2
+ import type { ImageUploader } from "../lib/markdown/image-pipeline";
3
+ import { type EditActivation } from "./edit/activation";
4
+ import { type EditCommit } from "./edit/outcome";
5
+ export interface InlineMarkdownEditProps extends EditActivation {
6
+ /** The committed markdown. Controlled — the field only ever holds a draft. */
7
+ value: string;
8
+ /**
9
+ * Commit a new value — the same contract as `InlineEdit.onCommit`: called
10
+ * only when the draft differs from `value`, with `base` = the value editing
11
+ * began from; return/resolve `{ refusal }` to refuse with a reason, throw or
12
+ * reject to report a failed save.
13
+ */
14
+ onCommit: EditCommit;
15
+ /**
16
+ * Whether the editor is open — REQUIRED, see the module comment. The field
17
+ * hands the closed state back through `onEditingChange(false)` once a commit
18
+ * lands, on Escape, and when asked to open while `readOnly`. Setting it
19
+ * `false` yourself drops the draft exactly as Escape does. A save already
20
+ * out when you do cannot be recalled: your `onCommit` promise still settles,
21
+ * but its outcome no longer touches the field — no message, no focus move,
22
+ * no `onEditingChange(false)` — and a commit from the next opening waits for
23
+ * it before starting.
24
+ */
25
+ editing: boolean;
26
+ /** The accessible name of the field and of its editor. */
27
+ label: string;
28
+ /** Shown at rest and in the editor when the value is empty. @default "Empty" */
29
+ placeholder?: string;
30
+ /** Collapse the rendered view to this many lines behind a `ReadMore`. */
31
+ collapsedLines?: number;
32
+ /** Extra toolbar content for the editor, after its built-in groups. */
33
+ toolbar?: ReactNode;
34
+ /** Stores a pasted/dropped/picked image — see `MarkdownEditorProps.uploadImage`. */
35
+ uploadImage?: ImageUploader;
36
+ /**
37
+ * This value cannot be edited at all, AND WHY — `InlineEdit.readOnly`'s
38
+ * shape and rule: asked to open, the field hands `onEditingChange(false)`
39
+ * straight back, and the reason is the rest view's accessible description.
40
+ */
41
+ readOnly?: {
42
+ reason: string;
43
+ };
44
+ /**
45
+ * What is shown when the commit REJECTS (never a `{ refusal }`, which
46
+ * renders its own text).
47
+ *
48
+ * @default "Could not save. Your text is still here — press Ctrl+Enter to try again."
49
+ */
50
+ failureMessage?: string;
51
+ /** The rejected error, handed on so it can be logged or reported. */
52
+ onError?: (error: unknown) => void;
53
+ className?: string;
54
+ }
55
+ export declare function InlineMarkdownEdit(props: InlineMarkdownEditProps): import("react").JSX.Element;
@@ -10,7 +10,20 @@ import { type ImageUploadState, type ImageUploader } from "../lib/markdown/image
10
10
  * contenteditable host, never a proxy element.
11
11
  */
12
12
  export interface MarkdownEditorHandle {
13
- focus: () => void;
13
+ /**
14
+ * Focus the surface. `caret` places the caret at the document's `"start"` or
15
+ * `"end"` (snapped to a real caret stop); omit it to leave the caret wherever
16
+ * the browser puts it. DX-3372: `InlineMarkdownEdit` opens with the caret at
17
+ * the end, the same place `InlineEdit` puts it.
18
+ *
19
+ * THROWS rather than returning quietly (DX-3372 review P0) when it cannot do
20
+ * what it was asked: the surface is not built yet (it is created in an
21
+ * effect, so a render or a layout effect is too early), the editor is
22
+ * read-only, focus did not land on the surface, or the caret could not be
23
+ * placed. A caller that goes on to assume focus is inside would otherwise
24
+ * wait for a blur that never comes.
25
+ */
26
+ focus: (caret?: "start" | "end") => void;
14
27
  }
15
28
  export interface MarkdownEditorProps {
16
29
  /** The markdown. Controlled when `onChange` is given. */
@@ -9,11 +9,11 @@ export declare const code: import("tailwind-variants").TVReturnType<{
9
9
  footer?: import("tailwind-variants").ClassValue;
10
10
  hint?: import("tailwind-variants").ClassValue;
11
11
  root?: import("tailwind-variants").ClassValue;
12
- control?: import("tailwind-variants").ClassValue;
13
12
  rendered?: import("tailwind-variants").ClassValue;
14
13
  layers?: import("tailwind-variants").ClassValue;
15
14
  gutter?: import("tailwind-variants").ClassValue;
16
15
  fade?: import("tailwind-variants").ClassValue;
16
+ control?: import("tailwind-variants").ClassValue;
17
17
  scroller?: import("tailwind-variants").ClassValue;
18
18
  expand?: import("tailwind-variants").ClassValue;
19
19
  };
@@ -29,11 +29,11 @@ export declare const code: import("tailwind-variants").TVReturnType<{
29
29
  footer?: import("tailwind-variants").ClassValue;
30
30
  hint?: import("tailwind-variants").ClassValue;
31
31
  root?: import("tailwind-variants").ClassValue;
32
- control?: import("tailwind-variants").ClassValue;
33
32
  rendered?: import("tailwind-variants").ClassValue;
34
33
  layers?: import("tailwind-variants").ClassValue;
35
34
  gutter?: import("tailwind-variants").ClassValue;
36
35
  fade?: import("tailwind-variants").ClassValue;
36
+ control?: import("tailwind-variants").ClassValue;
37
37
  scroller?: import("tailwind-variants").ClassValue;
38
38
  expand?: import("tailwind-variants").ClassValue;
39
39
  };
@@ -65,11 +65,11 @@ export declare const code: import("tailwind-variants").TVReturnType<{
65
65
  footer?: import("tailwind-variants").ClassValue;
66
66
  hint?: import("tailwind-variants").ClassValue;
67
67
  root?: import("tailwind-variants").ClassValue;
68
- control?: import("tailwind-variants").ClassValue;
69
68
  rendered?: import("tailwind-variants").ClassValue;
70
69
  layers?: import("tailwind-variants").ClassValue;
71
70
  gutter?: import("tailwind-variants").ClassValue;
72
71
  fade?: import("tailwind-variants").ClassValue;
72
+ control?: import("tailwind-variants").ClassValue;
73
73
  scroller?: import("tailwind-variants").ClassValue;
74
74
  expand?: import("tailwind-variants").ClassValue;
75
75
  };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The controlled activation pair every pencil-activated field takes, and the
3
+ * pair `EditGutter` hands out — so wiring a pencil to a field is one spread.
4
+ *
5
+ * `editing` says whether the field is open. The field calls
6
+ * `onEditingChange(false)` when it closes itself (a landed commit, Escape) and
7
+ * when it declines to open (read-only); it never calls `onEditingChange(true)`
8
+ * for an opening the caller made. The pair is all-or-nothing: a controlled
9
+ * `editing` with no way to hand the closed state back would leave the field
10
+ * shut after its first commit while the caller still believes it is open.
11
+ */
12
+ export interface EditActivation {
13
+ editing: boolean;
14
+ onEditingChange: (editing: boolean) => void;
15
+ }
16
+ /**
17
+ * Throws when a caller controls `editing` without `onEditingChange`. The
18
+ * types already forbid it; this covers a caller that got past them (a cast,
19
+ * a spread of `any`, plain JavaScript). DX-3372 review P0.
20
+ */
21
+ export declare function assertActivationPair(component: string, editing: boolean | undefined, onEditingChange: ((editing: boolean) => void) | undefined): void;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The caller's refusal of a value, with the reason that will be shown.
3
+ */
4
+ export interface EditRefusal {
5
+ refusal: string;
6
+ }
7
+ /** What `onCommit` may report. Returning nothing accepts the value. */
8
+ export type EditOutcome = void | EditRefusal;
9
+ /** Every in-place editor's `onCommit`: the new value and the value editing began from. */
10
+ export type EditCommit = (value: string, base: string) => EditOutcome | Promise<EditOutcome>;
11
+ export declare function isRefusal(outcome: EditOutcome): outcome is EditRefusal;
12
+ export declare function isPromise(outcome: EditOutcome | Promise<EditOutcome>): outcome is Promise<EditOutcome>;
13
+ /**
14
+ * The key that retries a failed save in a given field: Enter where Enter
15
+ * commits, Ctrl+Enter where Enter inserts a newline. (Both fields accept
16
+ * Cmd+Enter as well as Ctrl+Enter, so "Ctrl+Enter" is true on every platform.)
17
+ */
18
+ export type RetryKey = "Enter" | "Ctrl+Enter";
19
+ /**
20
+ * The default failure message. It names the retry key because blur never
21
+ * re-attempts a failed value — DX-3372 review L1: the single shared sentence
22
+ * said "press Enter" in fields where Enter inserts a newline.
23
+ */
24
+ export declare function defaultFailureMessage(retryKey: RetryKey): string;
25
+ /**
26
+ * A refusal nobody can read leaves the field open with nothing beside it — the
27
+ * exact silent failure the outcome contract exists to rule out — so an empty
28
+ * reason is a programming error, thrown rather than rendered.
29
+ */
30
+ export declare function assertReadableRefusal(component: string, reason: string, source: "validate" | "onCommit"): void;
31
+ /**
32
+ * THE MESSAGE IS AN OVERLAY, for the reason `InlineEdit` measured: in normal
33
+ * flow a refusal grows the control's own wrapper, which inside a fixed-height
34
+ * row shoves everything beside it. Absolutely positioned, it cannot contribute
35
+ * height. The parent must be `relative`.
36
+ */
37
+ export declare function OutcomeMessage({ id, children, className, }: {
38
+ id?: string;
39
+ children: string;
40
+ className?: string;
41
+ }): import("react").JSX.Element;
@@ -0,0 +1,55 @@
1
+ import { type EditCommit, type EditOutcome } from "./outcome";
2
+ export type SaveResult = {
3
+ outcome: EditOutcome;
4
+ } | {
5
+ error: unknown;
6
+ };
7
+ export interface EditSessionOptions {
8
+ /** Named in the thrown message when a refusal carries an empty reason. */
9
+ component: string;
10
+ /** The committed value — "unchanged" is measured against it. Read live. */
11
+ value: string;
12
+ onCommit: EditCommit;
13
+ onError?: (error: unknown) => void;
14
+ failureMessage: string;
15
+ /** End editing after a commit that landed (or had nothing to write). */
16
+ close: () => void;
17
+ /** Put focus back on the field after a refusal or a failure. */
18
+ refocus: () => void;
19
+ }
20
+ export interface EditSession {
21
+ pending: boolean;
22
+ refusal: string | null;
23
+ failure: string | null;
24
+ /** The message on screen, if any — a refusal or a failure. */
25
+ blocked: string | null;
26
+ /** The value this session began from — `onCommit`'s `base`. */
27
+ readonly base: string;
28
+ /** Start a new session. `refusal` seeds the message slot (live validation). */
29
+ begin: (base: string, refusal?: string | null) => void;
30
+ /** Finish the current session: every save still out becomes stale. */
31
+ end: () => void;
32
+ /** Commit `next` — unchanged check (see the module comment), `validate`, `onCommit`, outcome. */
33
+ commit: (next: string, validate?: (next: string) => string | null) => void;
34
+ refuse: (reason: string, source: "validate" | "onCommit") => void;
35
+ fail: (error: unknown) => void;
36
+ setPending: (pending: boolean) => void;
37
+ isPending: () => boolean;
38
+ setRefusal: (refusal: string | null) => void;
39
+ clearFailure: () => void;
40
+ /** The current session's token, for a caller that tracks its own save. */
41
+ token: () => number;
42
+ /** Whether a save from any session is still out. */
43
+ busy: () => boolean;
44
+ /** The in-flight save, settled or not — chain after it to stay in order. */
45
+ inFlight: () => Promise<void> | null;
46
+ /**
47
+ * Track a save sent under `token`. `onResult` runs only if that session is
48
+ * still current when it settles; otherwise `onStale` does (after a stale
49
+ * rejection has reached `onError`).
50
+ */
51
+ track: (save: Promise<EditOutcome>, token: number, onResult: (result: SaveResult) => void, onStale?: (result: SaveResult) => void) => void;
52
+ /** Run `fn` once no save is out — dropped if this session ends first. */
53
+ whenIdle: (fn: () => void) => void;
54
+ }
55
+ export declare function useEditSession(options: EditSessionOptions): EditSession;
@@ -21,7 +21,11 @@ export { Skeleton, SkeletonText, LoadingOverlay, type SkeletonProps, type Skelet
21
21
  export { ErrorBoundary, type ErrorBoundaryProps } from "./components/ErrorBoundary";
22
22
  export { Field, FieldSet, type FieldProps, type FieldSetProps } from "./components/Field";
23
23
  export { Input, Textarea, type InputProps, type TextareaProps } from "./components/Input";
24
- export { InlineEdit, type InlineEditProps, type InlineEditRefusal, type InlineEditOutcome, } from "./components/InlineEdit";
24
+ export { InlineEdit, type InlineEditProps } from "./components/InlineEdit";
25
+ export { EditGutter, type EditGutterProps } from "./components/EditGutter";
26
+ export { InlineMarkdownEdit, type InlineMarkdownEditProps } from "./components/InlineMarkdownEdit";
27
+ export type { EditActivation } from "./components/edit/activation";
28
+ export type { EditCommit, EditOutcome, EditRefusal } from "./components/edit/outcome";
25
29
  export { SecretField, type SecretFieldProps, type SecretValue } from "./components/SecretField";
26
30
  export { SettingRow, SettingGroup, type SettingRowProps, type SettingGroupProps, type SettingRowControlRenderProps, } from "./components/SettingRow";
27
31
  export { DescriptionList, DescriptionListItem, type DescriptionListProps, type DescriptionListItemProps, type DescriptionListEntry, } from "./components/DescriptionList";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danxbot/ui",
3
- "version": "11.0.0",
3
+ "version": "12.0.0",
4
4
  "type": "module",
5
5
  "description": "Danxbot — a domain-agnostic React design system with motion as a first-class primitive.",
6
6
  "license": "MIT",