@danxbot/ui 5.2.1 → 6.0.1

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.
@@ -2,168 +2,99 @@ import { type Ref } from "react";
2
2
  import { useRender } from "@base-ui/react/use-render";
3
3
  /**
4
4
  * The caller's refusal of a value, with the reason that will be shown.
5
- *
6
- * ONE OBJECT RATHER THAN TWO LOOSE RETURNS — `false` plus a separate message
7
- * prop, say — because those two can disagree: a refusal with no sentence beside
8
- * it is the mute rejection this whole component exists to prevent, and a
9
- * sentence with nothing refusing it is a lie. This library already settled that
10
- * shape three times over: `StatusOption.refusal`, `KanbanLane.refusal` and
11
- * `SettingRow.readOnly: { reason }`.
12
- *
13
- * Written for the person who typed it — "a demand with this name already
14
- * exists", not "invalid". It is the only thing they will see.
5
+ * Unchanged from 5.2.1 — see `InlineEditOutcome`.
15
6
  */
16
7
  export interface InlineEditRefusal {
17
8
  refusal: string;
18
9
  }
19
- /**
20
- * What `onCommit` may report. Returning nothing accepts the value.
21
- *
22
- * A REFUSAL IS NOT AN EXCEPTION, and keeping the two apart is the point: a
23
- * refusal is the caller's rule saying no (shown as its own sentence, the field
24
- * stays open for a correction), while a thrown error or a rejected promise is
25
- * the save FAILING (shown as `failureMessage`, the same text kept, retryable).
26
- * Folding them together would put a stack trace's message in front of an
27
- * end user, or bury a real outage as a validation note.
28
- */
10
+ /** What `onCommit` may report. Returning nothing accepts the value. */
29
11
  export type InlineEditOutcome = void | InlineEditRefusal;
30
12
  export interface InlineEditProps {
31
13
  /**
32
14
  * The committed value. Controlled — this component never holds the truth,
33
- * it only holds the draft while somebody is typing one.
15
+ * only the draft while somebody is typing one.
34
16
  */
35
17
  value: string;
36
18
  /**
37
- * Commit a new value. Called ONLY when the text actually differs from
38
- * `value`; see the module comment.
19
+ * Commit a new value. Called ONLY when the live text actually differs from
20
+ * `value`.
39
21
  *
40
- * May return a promise, in which case the field stays open and pending until
41
- * it settles. Resolve/return `{ refusal }` to reject the value with a reason;
42
- * throw or reject to report that the save itself failed.
22
+ * `base` is the `value` this control saw at the moment editing BEGAN —
23
+ * captured once and handed back unchanged regardless of how long the edit or
24
+ * the save takes. Compare it against whatever you currently believe is
25
+ * authoritative to tell "this would overwrite a change that landed while I
26
+ * was editing" from "nothing has moved". See the module comment
27
+ * ("THE OVERWRITE-PROTECTION CONTRACT") for the DX-2907 problem this closes.
43
28
  *
44
- * A SERVER'S VALIDATION RESPONSE IS A REFUSAL, NOT A FAILURE — catch it and
45
- * resolve to `{ refusal: <what the server said> }`. A duplicate name is the
46
- * caller's rule being applied by the only party who can check it, and it
47
- * reads to the person exactly like the local one; a rejected promise means
48
- * the write did not happen for a reason nobody can act on by retyping.
49
- *
50
- * WHICH RESPONSES THOSE ARE IS YOUR API'S BUSINESS, and worth checking
51
- * against a real one: the first integration's duplicate-name refusal arrived
52
- * as HTTP 400, so a handler keyed to 422 would have shown "Could not save"
53
- * for a name the server had merely declined.
29
+ * May return a promise, in which case the field stays open and pending until
30
+ * it settles. Resolve/return `{ refusal }` to reject the value with a
31
+ * reason; throw or reject to report that the save itself failed.
54
32
  */
55
- onCommit: (value: string) => InlineEditOutcome | Promise<InlineEditOutcome>;
33
+ onCommit: (value: string, base: string) => InlineEditOutcome | Promise<InlineEditOutcome>;
56
34
  /**
57
35
  * The refusals that are knowable WITHOUT a request. Return the reason, or
58
- * `null` to accept.
59
- *
60
- * Runs after the unchanged check and before `onCommit`, so a value that fails
61
- * here never reaches the network — which is the whole point of it being
62
- * separate: emptiness and length are decidable locally, uniqueness is not,
63
- * and a client that guesses at the second one drifts from the server.
64
- *
65
- * A BARE STRING RATHER THAN `{ refusal }`, unlike `onCommit`'s return, and
66
- * the difference is not an inconsistency: `onCommit` has to say WHICH of
67
- * accepted-or-refused happened, so the reason needs a name to sit under.
68
- * Everything this function returns is the reason, which is the same position
69
- * `StatusOption.refusal` and `KanbanLane.refusal` are already a bare string
70
- * in. An empty string throws, for the reason those do.
71
- *
72
- * It is shown through exactly the same channel a refusal from `onCommit` is —
73
- * the person is not told where in the stack their name was turned down.
36
+ * `null` to accept. Runs after the unchanged check and before `onCommit`.
74
37
  */
75
38
  validate?: (value: string) => string | null;
76
39
  /**
77
- * The accessible name, in BOTH modes. REQUIRED — see the module comment on
78
- * why a control that renames itself mid-interaction is the defect this
79
- * prevents.
80
- *
81
- * Name the FIELD ("Demand title"), not the action ("Edit") and not the
82
- * current value: it is read in a list of controls where the value is
83
- * announced separately.
40
+ * The accessible name AND accessible value's label, in both states —
41
+ * `role="textbox"` computes its accessible value from its own text content,
42
+ * so this is the one thing that has to be said separately. REQUIRED.
84
43
  */
85
44
  label: string;
86
45
  /**
87
- * What the display shows in place of a blank value, and the field's
88
- * placeholder.
46
+ * What the control shows in place of a blank value, and its
47
+ * `aria-placeholder`.
89
48
  *
90
- * A WORD, not an em-dash, and not nothing: a display whose entire content is
91
- * punctuation has no name worth announcing, and one whose content is empty
92
- * collapses to a few pixels — a control that cannot be hit. See
93
- * `scripts/probe-card-header.mjs` for the zero-width family this belongs to.
49
+ * @default "Empty"
94
50
  */
95
51
  placeholder?: string;
96
- /** Box ramp. Matches `Input`'s names; see the recipe on why not its padding. */
52
+ /** Box ramp. Matches `Input`'s names. */
97
53
  size?: "sm" | "md" | "lg";
98
54
  /**
99
- * Edit as a `<textarea>` that grows with its content.
55
+ * Edit as a growing multi-line block instead of a single line.
100
56
  *
101
- * THE COMMIT KEY MOVES, because Enter has to insert a newline. Enter is a
102
- * newline, Cmd/Ctrl+Enter commits, Escape still cancels and blur still
103
- * commits. Nothing else changes.
57
+ * THE COMMIT KEY MOVES, because Enter has to insert a newline instead: Enter
58
+ * inserts "\n", Cmd/Ctrl+Enter commits, Escape still cancels, blur still
59
+ * commits.
104
60
  */
105
61
  multiline?: boolean;
106
62
  /**
107
- * This value cannot be edited at all, AND WHY — a different fact from a
108
- * value being refused, and it belongs on a different prop. `{ reason }`
109
- * matches `SettingRow.readOnly`; there is no boolean form, because a control
110
- * that refuses in silence is indistinguishable from one that is broken.
111
- *
112
- * The display stays focusable and `aria-disabled`, carrying the reason as
113
- * its description and in a tooltip, so it can be asked.
63
+ * This value cannot be edited at all, AND WHY. `{ reason }`, matching
64
+ * `SettingRow.readOnly` — a control that refuses in silence is
65
+ * indistinguishable from one that is broken.
114
66
  */
115
67
  readOnly?: {
116
68
  reason: string;
117
69
  };
118
70
  /**
119
- * What is shown when the commit REJECTS. Deliberately not the error's own
120
- * message: that text is written for a log, and this one is read by whoever
121
- * just typed a name.
71
+ * What is shown when the commit REJECTS (a thrown error or a rejected
72
+ * promise — never a `{ refusal }`, which renders its own text).
122
73
  *
123
74
  * @default "Could not save. Your text is still here — press Enter to try again."
124
75
  */
125
76
  failureMessage?: string;
126
- /**
127
- * The rejected error, handed on so it can be logged or reported. Without
128
- * this the exception is swallowed entirely, which is the silent failure this
129
- * library treats as a defect rather than as tidiness.
130
- */
77
+ /** The rejected error, handed on so it can be logged or reported. */
131
78
  onError?: (error: unknown) => void;
132
79
  /**
133
- * Render the DISPLAY as a different element. See the module comment: the
134
- * rendered element is given `role="button"`, which replaces its own role.
80
+ * Fires exactly when editing starts and exactly when it ends — never while a
81
+ * read-only control merely declines to open. Use it to suspend any
82
+ * re-render-from-server logic of your own for as long as someone is
83
+ * actually looking at the field; see the module comment.
135
84
  */
85
+ onEditingChange?: (editing: boolean) => void;
86
+ /** Render as a different element — see `Button`'s own `render` for the shape. */
136
87
  render?: useRender.RenderProp;
137
88
  /**
138
- * Applied to BOTH forms, and there is only one of these on purpose. Anything
139
- * that changes one box's geometry has to change the other's, or the control
140
- * jumps on click — a second `fieldClassName` would make that jump one prop
141
- * away at all times.
142
- *
143
- * TYPOGRAPHY REACHES BOTH FORMS AND DOES LAND — `font-medium`, `text-lg`,
144
- * `tracking-tight` all work. That is worth saying because for one release it
145
- * silently did not: the shared box declared `[font:inherit]` as a utility,
146
- * the `font` shorthand resets weight, and Tailwind's emission order gave the
147
- * shorthand the win, so `font-medium` computed as 400 with no error. The
148
- * declaration now lives in `@layer components` (`dx-inline-type`), where
149
- * every utility of equal specificity beats it.
89
+ * Applied in both states, and there is only one of these on purpose — see
90
+ * `box` above: anything that changes the box's geometry has to reach both
91
+ * states or the control shifts on click, and a control cannot shift on click
92
+ * when both states are the same element unless a class only reaches one of
93
+ * them.
150
94
  */
151
95
  className?: string;
152
- /**
153
- * The refusal/failure message only — NOT the box.
154
- *
155
- * A separate prop here where `className` deliberately refuses to have one,
156
- * and the difference is the coupling: the display and the field must measure
157
- * identically, so their classes cannot be set apart; the message is out of
158
- * flow and coupled to neither.
159
- *
160
- * What it is FOR: the message is absolutely positioned below the control, so
161
- * an `overflow: hidden` ancestor clips it and a control at the bottom of a
162
- * panel points it off the edge. Reposition it (`bottom-full mb-1` to flip
163
- * above), or drop `absolute` where nothing is fixed-height and the growth is
164
- * fine. Without this there was no way to do either — reported as exactly that.
165
- */
96
+ /** The refusal/failure message only — NOT the box. See 5.2.1's own doc for why it is a separate prop. */
166
97
  messageClassName?: string;
167
98
  ref?: Ref<HTMLElement>;
168
99
  }
169
- export declare function InlineEdit({ value, onCommit, validate, label, placeholder, size, multiline, readOnly, failureMessage, onError, render, className, messageClassName, ref, }: InlineEditProps): import("react").JSX.Element;
100
+ export declare function InlineEdit({ value, onCommit, validate, label, placeholder, size, multiline, readOnly, failureMessage, onError, onEditingChange, render, className, messageClassName, ref, }: InlineEditProps): import("react").JSX.Element;
@@ -1,4 +1,16 @@
1
- import { type ReactNode } from "react";
1
+ import { type ReactNode, type Ref } from "react";
2
+ /**
3
+ * The imperative half of the DX-2930 / DX-2907-problem-137 contract: a
4
+ * consumer that needs to move focus into the surface from code (a "Edit"
5
+ * button, a just-opened panel) gets a NAMED method instead of reaching for
6
+ * `data-md-editable` inside the page — the internal it was reduced to
7
+ * querying for before this existed. `focus()` mirrors what the toolbar's own
8
+ * `apply()` already does after a command: `editor.root.focus()`, the actual
9
+ * contenteditable host, never a proxy element.
10
+ */
11
+ export interface MarkdownEditorHandle {
12
+ focus: () => void;
13
+ }
2
14
  export interface MarkdownEditorProps {
3
15
  /** The markdown. Controlled when `onChange` is given. */
4
16
  value: string;
@@ -20,5 +32,13 @@ export interface MarkdownEditorProps {
20
32
  className?: string;
21
33
  /** Height of the editable area. Grows with content when omitted. */
22
34
  minHeight?: number | string;
35
+ /**
36
+ * Focus the surface once, on mount — the same one-shot semantics as an
37
+ * `<input autoFocus>`. Ignored on `readOnly`, since there is nothing to
38
+ * focus into.
39
+ */
40
+ autoFocus?: boolean;
41
+ /** Imperative focus handle — see `MarkdownEditorHandle`. */
42
+ ref?: Ref<MarkdownEditorHandle>;
23
43
  }
24
- export declare function MarkdownEditor({ value, onChange, readOnly, label, hideLabel, placeholder, toolbar, className, minHeight, }: MarkdownEditorProps): import("react").JSX.Element;
44
+ export declare function MarkdownEditor({ value, onChange, readOnly, label, hideLabel, placeholder, toolbar, className, minHeight, autoFocus, ref, }: MarkdownEditorProps): import("react").JSX.Element;
@@ -0,0 +1,76 @@
1
+ import { type ReactNode, type Ref } from "react";
2
+ declare const readMore: import("tailwind-variants").TVReturnType<{
3
+ [key: string]: {
4
+ [key: string]: import("tailwind-variants").ClassValue | {
5
+ bar?: import("tailwind-variants").ClassValue;
6
+ content?: import("tailwind-variants").ClassValue;
7
+ toggle?: import("tailwind-variants").ClassValue;
8
+ root?: import("tailwind-variants").ClassValue;
9
+ viewport?: import("tailwind-variants").ClassValue;
10
+ };
11
+ };
12
+ } | {
13
+ [x: string]: {
14
+ [x: string]: import("tailwind-variants").ClassValue | {
15
+ bar?: import("tailwind-variants").ClassValue;
16
+ content?: import("tailwind-variants").ClassValue;
17
+ toggle?: import("tailwind-variants").ClassValue;
18
+ root?: import("tailwind-variants").ClassValue;
19
+ viewport?: import("tailwind-variants").ClassValue;
20
+ };
21
+ };
22
+ } | {}, {
23
+ root: string;
24
+ viewport: string[];
25
+ content: string;
26
+ bar: string;
27
+ toggle: string;
28
+ }, undefined, {
29
+ [key: string]: {
30
+ [key: string]: import("tailwind-variants").ClassValue | {
31
+ bar?: import("tailwind-variants").ClassValue;
32
+ content?: import("tailwind-variants").ClassValue;
33
+ toggle?: import("tailwind-variants").ClassValue;
34
+ root?: import("tailwind-variants").ClassValue;
35
+ viewport?: import("tailwind-variants").ClassValue;
36
+ };
37
+ };
38
+ } | {}, {
39
+ root: string;
40
+ viewport: string[];
41
+ content: string;
42
+ bar: string;
43
+ toggle: string;
44
+ }, import("tailwind-variants").TVReturnTypeLike<unknown, {
45
+ root: string;
46
+ viewport: string[];
47
+ content: string;
48
+ bar: string;
49
+ toggle: string;
50
+ }>>;
51
+ export interface ReadMoreProps {
52
+ /** Collapsed line count — how many rendered lines show before the fade strip. Must be a positive integer; anything else throws. */
53
+ lines: number;
54
+ /** Typically rendered Markdown — paragraphs, headings, lists, code, images. See the module comment on why the clamp is a measured line boundary rather than a computed height. */
55
+ children: ReactNode;
56
+ /** Controlled expanded state. Omit to let the component hold its own (seeded by `defaultExpanded`). */
57
+ expanded?: boolean;
58
+ /** Uncontrolled initial state. Ignored once `expanded` is supplied. @default false */
59
+ defaultExpanded?: boolean;
60
+ /** Fires on every toggle, controlled or not. */
61
+ onExpandedChange?: (expanded: boolean) => void;
62
+ /** @default "Read more" */
63
+ moreLabel?: string;
64
+ /** @default "Show less" */
65
+ lessLabel?: string;
66
+ className?: string;
67
+ ref?: Ref<HTMLDivElement>;
68
+ }
69
+ /**
70
+ * Clamp block content to `lines` rendered lines behind a fade strip and a
71
+ * single "Read more" / "Show less" toggle. Renders neither the strip nor the
72
+ * toggle when the content already fits — see the module comment for how that
73
+ * is decided.
74
+ */
75
+ export declare function ReadMore({ lines, children, expanded: expandedProp, defaultExpanded, onExpandedChange, moreLabel, lessLabel, className, ref, }: ReadMoreProps): import("react").JSX.Element;
76
+ export { readMore as readMoreVariants };
@@ -45,6 +45,15 @@ export interface ProvenanceDetailProps {
45
45
  * consumer putting its editor in `actions`, which reads as "here is the
46
46
  * value, and separately, down here, a form about some value".
47
47
  *
48
+ * AN `InlineEdit` HERE STILL NEEDS THE OVERWRITE-PROTECTION CONTRACT IT
49
+ * SHIPS WITH (DX-2930) — this panel can stay open while the underlying
50
+ * record keeps changing (a re-extraction, another reviewer's own edit), so
51
+ * an `onCommit={(next, base) => …}` that compares `base` against whatever
52
+ * the caller currently believes is authoritative, plus an
53
+ * `onEditingChange` that suspends re-rendering `record` while someone is
54
+ * mid-edit, is what stops this panel from resetting a draft out from under
55
+ * whoever opened it. See `content/docs/provenance.mdx` for the shape.
56
+ *
48
57
  * IT OVERRIDES THE ABSENCES TOO — redacted, empty and failed alike. A slot
49
58
  * that disappeared whenever the value was missing would vanish in exactly the
50
59
  * case a value editor is most wanted: a field that was never extracted is the
@@ -57,7 +57,8 @@ export { Progress, Meter, ProgressRing, type ProgressProps, type MeterProps, typ
57
57
  export { Stat, StatRow, Sparkline, type StatProps, type StatRowProps, type SparklineProps, type StatSize, type StatTrend, type StatSentiment, } from "./components/Stat";
58
58
  export { DataTable, TableEmptyState, CellStack, type DataTableProps, type TableDensity, } from "./components/Table";
59
59
  export { CodeEditor, CodeEditorToolbar, CodeEditorCaption, CodeEditorCopyButton, CodeEditorWrapToggle, CodeEditorViewSwitch, type CodeEditorProps, type CodeEditorToolbarProps, type CodeEditorView, } from "./components/CodeEditor";
60
- export { MarkdownEditor, type MarkdownEditorProps } from "./components/MarkdownEditor";
60
+ export { MarkdownEditor, type MarkdownEditorProps, type MarkdownEditorHandle, } from "./components/MarkdownEditor";
61
+ export { ReadMore, readMoreVariants, type ReadMoreProps } from "./components/ReadMore";
61
62
  export { MarkdownSurface, type SurfaceOptions, type Splice } from "./lib/markdown/surface";
62
63
  export { project, projectedText, ProjectionError } from "./lib/markdown/project";
63
64
  export type { Projection, PNode, PText, PElement } from "./lib/markdown/project";
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from "react";
1
+ import { type ReactNode } from "react";
2
2
  import type { MdCodeRenderer, MdNode } from "./types";
3
3
  export interface MarkdownNodesProps {
4
4
  nodes: MdNode[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danxbot/ui",
3
- "version": "5.2.1",
3
+ "version": "6.0.1",
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",
@@ -73,7 +73,7 @@
73
73
  "gates": "npm run shoot && npm run a11y && node scripts/shoot-overlays.mjs && npm run probe",
74
74
  "a11y": "node scripts/a11y-scan.mjs",
75
75
  "shoot": "node scripts/shoot.mjs",
76
- "probe": "node scripts/probe-scale.mjs && node scripts/probe-switch.mjs && node scripts/probe-slider.mjs && node scripts/probe-panel.mjs && node scripts/probe-popover-width.mjs && node scripts/probe-api.mjs && node scripts/probe-api-live.mjs && node scripts/probe-tabs-overflow.mjs && node scripts/probe-stat-row.mjs && node scripts/probe-packaging.mjs && node scripts/probe-containment.mjs && node scripts/probe-popover-containment.mjs && node scripts/probe-shell.mjs && node scripts/probe-card-header.mjs && node scripts/probe-inline-edit.mjs",
76
+ "probe": "node scripts/probe-scale.mjs && node scripts/probe-switch.mjs && node scripts/probe-slider.mjs && node scripts/probe-panel.mjs && node scripts/probe-popover-width.mjs && node scripts/probe-api.mjs && node scripts/probe-api-live.mjs && node scripts/probe-tabs-overflow.mjs && node scripts/probe-stat-row.mjs && node scripts/probe-packaging.mjs && node scripts/probe-containment.mjs && node scripts/probe-popover-containment.mjs && node scripts/probe-shell.mjs && node scripts/probe-card-header.mjs && node scripts/probe-inline-edit.mjs && node scripts/probe-read-more.mjs",
77
77
  "sync": "node scripts/sync.mjs",
78
78
  "visual": "node scripts/visual-docker.mjs",
79
79
  "visual:update": "node scripts/visual-docker.mjs --update-snapshots",