@mindlogic-ai/logician-ui 4.0.0-alpha.36 → 4.0.0-alpha.38
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.
- package/USAGE.md +17 -0
- package/dist/components/Form/FormSection/CollapsibleFormSection.js +1 -0
- package/dist/components/Form/FormSection/CollapsibleFormSection.js.map +1 -1
- package/dist/components/Form/FormSection/CollapsibleFormSection.mjs +1 -0
- package/dist/components/Form/FormSection/CollapsibleFormSection.mjs.map +1 -1
- package/dist/components/Toast/Toast.d.ts +7 -3
- package/dist/components/Toast/Toast.d.ts.map +1 -1
- package/dist/components/Toast/Toast.js +7 -3
- package/dist/components/Toast/Toast.js.map +1 -1
- package/dist/components/Toast/Toast.mjs +8 -4
- package/dist/components/Toast/Toast.mjs.map +1 -1
- package/dist/components/Toast/Toast.styles.d.ts +8 -0
- package/dist/components/Toast/Toast.styles.d.ts.map +1 -1
- package/dist/components/Toast/Toast.styles.js +59 -0
- package/dist/components/Toast/Toast.styles.js.map +1 -1
- package/dist/components/Toast/Toast.styles.mjs +57 -1
- package/dist/components/Toast/Toast.styles.mjs.map +1 -1
- package/dist/components/Toast/Toast.types.d.ts +25 -1
- package/dist/components/Toast/Toast.types.d.ts.map +1 -1
- package/dist/components/Toast/Toaster.d.ts.map +1 -1
- package/dist/components/Toast/Toaster.js +9 -1
- package/dist/components/Toast/Toaster.js.map +1 -1
- package/dist/components/Toast/Toaster.mjs +9 -1
- package/dist/components/Toast/Toaster.mjs.map +1 -1
- package/dist/components/Toast/useToast.d.ts +1 -1
- package/dist/components/Toast/useToast.d.ts.map +1 -1
- package/dist/components/Toast/useToast.js +9 -1
- package/dist/components/Toast/useToast.js.map +1 -1
- package/dist/components/Toast/useToast.mjs +10 -2
- package/dist/components/Toast/useToast.mjs.map +1 -1
- package/dist/components/Workflow/Workflow.d.ts +1 -1
- package/dist/components/Workflow/Workflow.d.ts.map +1 -1
- package/dist/components/Workflow/Workflow.js +2 -2
- package/dist/components/Workflow/Workflow.js.map +1 -1
- package/dist/components/Workflow/Workflow.mjs +2 -2
- package/dist/components/Workflow/Workflow.mjs.map +1 -1
- package/dist/components/Workflow/Workflow.types.d.ts +9 -0
- package/dist/components/Workflow/Workflow.types.d.ts.map +1 -1
- package/dist/components/Workflow/Workflow.types.js.map +1 -1
- package/dist/components/Workflow/Workflow.types.mjs.map +1 -1
- package/dist/components/Workflow/canvas/Canvas/Canvas.d.ts +6 -2
- package/dist/components/Workflow/canvas/Canvas/Canvas.d.ts.map +1 -1
- package/dist/components/Workflow/canvas/Canvas/Canvas.js +29 -23
- package/dist/components/Workflow/canvas/Canvas/Canvas.js.map +1 -1
- package/dist/components/Workflow/canvas/Canvas/Canvas.mjs +31 -25
- package/dist/components/Workflow/canvas/Canvas/Canvas.mjs.map +1 -1
- package/dist/components/Workflow/createNode.d.ts +25 -1
- package/dist/components/Workflow/createNode.d.ts.map +1 -1
- package/dist/components/Workflow/createNode.js +39 -1
- package/dist/components/Workflow/createNode.js.map +1 -1
- package/dist/components/Workflow/createNode.mjs +38 -2
- package/dist/components/Workflow/createNode.mjs.map +1 -1
- package/dist/components/Workflow/index.d.ts +2 -0
- package/dist/components/Workflow/index.d.ts.map +1 -1
- package/dist/components/Workflow/layout/autoLayout.d.ts +10 -0
- package/dist/components/Workflow/layout/autoLayout.d.ts.map +1 -1
- package/dist/components/Workflow/layout/autoLayout.js +4 -0
- package/dist/components/Workflow/layout/autoLayout.js.map +1 -1
- package/dist/components/Workflow/layout/autoLayout.mjs +1 -1
- package/dist/components/Workflow/layout/autoLayout.mjs.map +1 -1
- package/dist/components/Workflow/placement.d.ts +65 -0
- package/dist/components/Workflow/placement.d.ts.map +1 -0
- package/dist/components/Workflow/placement.js +100 -0
- package/dist/components/Workflow/placement.js.map +1 -0
- package/dist/components/Workflow/placement.mjs +96 -0
- package/dist/components/Workflow/placement.mjs.map +1 -0
- package/dist/components/Workflow/useWorkflowActions.d.ts +54 -0
- package/dist/components/Workflow/useWorkflowActions.d.ts.map +1 -0
- package/dist/components/Workflow/useWorkflowActions.js +105 -0
- package/dist/components/Workflow/useWorkflowActions.js.map +1 -0
- package/dist/components/Workflow/useWorkflowActions.mjs +103 -0
- package/dist/components/Workflow/useWorkflowActions.mjs.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/Toast/Toast.action.test.tsx +135 -0
- package/src/components/Toast/Toast.stories.tsx +111 -0
- package/src/components/Toast/Toast.styles.ts +60 -0
- package/src/components/Toast/Toast.tsx +16 -3
- package/src/components/Toast/Toast.types.ts +26 -1
- package/src/components/Toast/Toaster.tsx +11 -0
- package/src/components/Toast/useToast.tsx +17 -2
- package/src/components/Workflow/README.md +64 -0
- package/src/components/Workflow/Workflow.tsx +2 -0
- package/src/components/Workflow/Workflow.types.ts +10 -0
- package/src/components/Workflow/canvas/Canvas/Canvas.tsx +57 -53
- package/src/components/Workflow/createNode.test.ts +40 -2
- package/src/components/Workflow/createNode.ts +70 -2
- package/src/components/Workflow/index.ts +8 -0
- package/src/components/Workflow/layout/autoLayout.ts +4 -4
- package/src/components/Workflow/placement.test.ts +156 -0
- package/src/components/Workflow/placement.ts +172 -0
- package/src/components/Workflow/stories/Workflow.stories.tsx +79 -0
- package/src/components/Workflow/useWorkflowActions.test.tsx +345 -0
- package/src/components/Workflow/useWorkflowActions.ts +196 -0
- package/src/guidelines/FeedbackActions.mdx +133 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo } from 'react';
|
|
4
|
+
import { useReactFlow, useStoreApi } from '@xyflow/react';
|
|
5
|
+
|
|
6
|
+
import { createNodeFromType, genId, toDefaultsTranslate } from './createNode';
|
|
7
|
+
import {
|
|
8
|
+
type AddNodeTarget,
|
|
9
|
+
FALLBACK_NODE_SIZE,
|
|
10
|
+
findFreeExit,
|
|
11
|
+
planNodePlacement,
|
|
12
|
+
} from './placement';
|
|
13
|
+
import { useWorkflow } from './WorkflowContext';
|
|
14
|
+
|
|
15
|
+
export type { AddNodeTarget } from './placement';
|
|
16
|
+
|
|
17
|
+
/** What `addNode` did: the new node's id and the auto-connect edge's, if any. */
|
|
18
|
+
export type AddNodeResult = { nodeId: string; edgeId: string | null };
|
|
19
|
+
|
|
20
|
+
export type AddNodeOptions = {
|
|
21
|
+
/**
|
|
22
|
+
* What the inspector does once the new node is selected.
|
|
23
|
+
* - `'follow'` (default): an open inspector moves to the new node, a closed
|
|
24
|
+
* one stays closed — the same rule drag-select follows.
|
|
25
|
+
* - `'open'`: open it on the new node (what a click does).
|
|
26
|
+
* - `'close'`: close it, e.g. on a phone where it would cover the canvas.
|
|
27
|
+
*/
|
|
28
|
+
inspector?: 'follow' | 'open' | 'close';
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** The exit a node added `{ after: nodeId }` would be wired from. */
|
|
32
|
+
export type AddAfterSource = {
|
|
33
|
+
nodeId: string;
|
|
34
|
+
sourceHandle: string;
|
|
35
|
+
/** Display name: the node's instance title, else its type label. */
|
|
36
|
+
label: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type WorkflowActions = {
|
|
40
|
+
/**
|
|
41
|
+
* Add a node of `kind` with the same id and starting config a palette drop
|
|
42
|
+
* produces, place it per `target` (see `AddNodeTarget`), select it and pan
|
|
43
|
+
* to it. Node + auto-edge land as one history step, so one undo reverts
|
|
44
|
+
* both. Returns `null` (and changes nothing) when the editor is read-only,
|
|
45
|
+
* `kind` is not registered, or the kind's id is taken (a singleton `start`).
|
|
46
|
+
*/
|
|
47
|
+
addNode: (
|
|
48
|
+
kind: string,
|
|
49
|
+
target: AddNodeTarget,
|
|
50
|
+
options?: AddNodeOptions
|
|
51
|
+
) => AddNodeResult | null;
|
|
52
|
+
/**
|
|
53
|
+
* The exit `addNode(kind, { after: nodeId })` would wire from, or `null` when
|
|
54
|
+
* the node has none free — the add would land unconnected at the viewport
|
|
55
|
+
* centre. Use it to label the action ("Add after ‹label›" vs "Add at
|
|
56
|
+
* centre"). Whether a given kind then accepts the edge is still up to its
|
|
57
|
+
* handles and `canConnect`; `addNode`'s `edgeId` reports the outcome.
|
|
58
|
+
*/
|
|
59
|
+
getAddAfterSource: (
|
|
60
|
+
nodeId: string | null | undefined
|
|
61
|
+
) => AddAfterSource | null;
|
|
62
|
+
/** `getAddAfterSource(nodeId) !== null`. */
|
|
63
|
+
canAddAfter: (nodeId: string | null | undefined) => boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Pan duration when `addNode` brings the new node into view. */
|
|
67
|
+
const PAN_MS = 300;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Imperative editor actions for content rendered inside `<Workflow>` (its
|
|
71
|
+
* `children` and render slots sit inside the canvas's React Flow provider).
|
|
72
|
+
* Hosts use this instead of re-implementing node creation and placement — the
|
|
73
|
+
* tap-to-add palette on touch devices being the first caller.
|
|
74
|
+
*
|
|
75
|
+
* Must be called from a component rendered inside `<Workflow>`.
|
|
76
|
+
*/
|
|
77
|
+
export function useWorkflowActions(): WorkflowActions {
|
|
78
|
+
const {
|
|
79
|
+
graph,
|
|
80
|
+
dispatch,
|
|
81
|
+
getNodeType,
|
|
82
|
+
translate,
|
|
83
|
+
hostBridge,
|
|
84
|
+
readOnly,
|
|
85
|
+
editor: { drawerTarget },
|
|
86
|
+
setSelectedNodeId,
|
|
87
|
+
setSelectedEdgeId,
|
|
88
|
+
setDrawerTarget,
|
|
89
|
+
} = useWorkflow();
|
|
90
|
+
const { getNode, getViewport, setCenter } = useReactFlow();
|
|
91
|
+
const store = useStoreApi();
|
|
92
|
+
|
|
93
|
+
const getAddAfterSource = useCallback(
|
|
94
|
+
(nodeId: string | null | undefined): AddAfterSource | null => {
|
|
95
|
+
const exit = findFreeExit(graph, getNodeType, nodeId);
|
|
96
|
+
if (!exit) return null;
|
|
97
|
+
const def = getNodeType(exit.node.kind);
|
|
98
|
+
const label =
|
|
99
|
+
def?.getInstanceTitle?.(exit.node.config) || def?.label || exit.node.id;
|
|
100
|
+
return { nodeId: exit.node.id, sourceHandle: exit.sourceHandle, label };
|
|
101
|
+
},
|
|
102
|
+
[graph, getNodeType]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const canAddAfter = useCallback(
|
|
106
|
+
(nodeId: string | null | undefined) => getAddAfterSource(nodeId) !== null,
|
|
107
|
+
[getAddAfterSource]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const addNode = useCallback(
|
|
111
|
+
(
|
|
112
|
+
kind: string,
|
|
113
|
+
target: AddNodeTarget,
|
|
114
|
+
{ inspector = 'follow' }: AddNodeOptions = {}
|
|
115
|
+
): AddNodeResult | null => {
|
|
116
|
+
if (readOnly) return null;
|
|
117
|
+
const def = getNodeType(kind);
|
|
118
|
+
if (!def) return null;
|
|
119
|
+
|
|
120
|
+
const draft = createNodeFromType(def, {
|
|
121
|
+
position: { x: 0, y: 0 },
|
|
122
|
+
nodes: graph.nodes,
|
|
123
|
+
translate: toDefaultsTranslate(translate),
|
|
124
|
+
hostBridge,
|
|
125
|
+
});
|
|
126
|
+
// `nextNodeId` returns a fixed id for singletons (`start`); a second one
|
|
127
|
+
// would be dropped by the reducer's dedupe, so report it instead.
|
|
128
|
+
if (graph.nodes.some((n) => n.id === draft.id)) return null;
|
|
129
|
+
|
|
130
|
+
// Centre of the visible pane, in flow coordinates.
|
|
131
|
+
const { width, height } = store.getState();
|
|
132
|
+
const viewport = getViewport();
|
|
133
|
+
const viewportCenter = {
|
|
134
|
+
x: (width / 2 - viewport.x) / viewport.zoom,
|
|
135
|
+
y: (height / 2 - viewport.y) / viewport.zoom,
|
|
136
|
+
};
|
|
137
|
+
const { position, edge } = planNodePlacement({
|
|
138
|
+
graph,
|
|
139
|
+
getNodeType,
|
|
140
|
+
node: draft,
|
|
141
|
+
target,
|
|
142
|
+
viewportCenter,
|
|
143
|
+
edgeId: genId('e'),
|
|
144
|
+
getNodeSize: (id) => {
|
|
145
|
+
const { width: w, height: h } = getNode(id)?.measured ?? {};
|
|
146
|
+
return w && h ? { width: w, height: h } : undefined;
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
const node = { ...draft, position };
|
|
150
|
+
|
|
151
|
+
dispatch({
|
|
152
|
+
type: 'addNodeWithEdge',
|
|
153
|
+
node,
|
|
154
|
+
autoConnectFrom: edge ? { edge } : null,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Select the new node so a follow-up `{ after: selected }` chains on.
|
|
158
|
+
setSelectedNodeId(node.id);
|
|
159
|
+
setSelectedEdgeId(null);
|
|
160
|
+
if (inspector === 'open' || (inspector === 'follow' && drawerTarget)) {
|
|
161
|
+
setDrawerTarget({ type: 'node', id: node.id });
|
|
162
|
+
} else if (inspector === 'close') {
|
|
163
|
+
setDrawerTarget(null);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
void setCenter(
|
|
167
|
+
position.x + FALLBACK_NODE_SIZE.width / 2,
|
|
168
|
+
position.y + FALLBACK_NODE_SIZE.height / 2,
|
|
169
|
+
{ zoom: viewport.zoom, duration: PAN_MS }
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
return { nodeId: node.id, edgeId: edge?.id ?? null };
|
|
173
|
+
},
|
|
174
|
+
[
|
|
175
|
+
readOnly,
|
|
176
|
+
getNodeType,
|
|
177
|
+
graph,
|
|
178
|
+
translate,
|
|
179
|
+
hostBridge,
|
|
180
|
+
store,
|
|
181
|
+
getViewport,
|
|
182
|
+
getNode,
|
|
183
|
+
dispatch,
|
|
184
|
+
setSelectedNodeId,
|
|
185
|
+
setSelectedEdgeId,
|
|
186
|
+
drawerTarget,
|
|
187
|
+
setDrawerTarget,
|
|
188
|
+
setCenter,
|
|
189
|
+
]
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
return useMemo(
|
|
193
|
+
() => ({ addNode, getAddAfterSource, canAddAfter }),
|
|
194
|
+
[addNode, getAddAfterSource, canAddAfter]
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Meta } from '@storybook/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Guidelines/Actions in feedback surfaces" />
|
|
4
|
+
|
|
5
|
+
# Actions in feedback surfaces
|
|
6
|
+
|
|
7
|
+
Toasts, inline banners, panes and dialogs often need a control: "buy credits",
|
|
8
|
+
"undo", "open file". This page covers which control goes where and how it
|
|
9
|
+
should look.
|
|
10
|
+
|
|
11
|
+
The test for every case: **does the control look like it belongs to the
|
|
12
|
+
container it sits in?**
|
|
13
|
+
|
|
14
|
+
> Visual prototype with before/after and a playground:
|
|
15
|
+
> [feedback-actions-proto.apps.mindlogic.tools](https://feedback-actions-proto.apps.mindlogic.tools/)
|
|
16
|
+
> (company login).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Why this exists
|
|
21
|
+
|
|
22
|
+
Until `Toast` had an `action` prop, callers who wanted a button in a toast put
|
|
23
|
+
a `<Button>` inside `description` and styled it themselves. On the credit-402
|
|
24
|
+
toast in FactChat that produced a neutral outline button (`#494949` text, ink
|
|
25
|
+
hairline) on a rose toast (`#FFE6E8` background, `#69000A` text). It carried
|
|
26
|
+
none of the toast's colors, so it read as a separate card dropped on top.
|
|
27
|
+
|
|
28
|
+
The fix is one recipe for every toast: the action uses the toast's own text
|
|
29
|
+
color, the same treatment as the close ×.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Rules
|
|
34
|
+
|
|
35
|
+
### 1. Match the container's weight
|
|
36
|
+
|
|
37
|
+
Toasts and inline banners carry **text actions**. Buttons belong to surfaces
|
|
38
|
+
that own the space: a pane, a dialog, an empty state.
|
|
39
|
+
|
|
40
|
+
### 2. The action takes the status color
|
|
41
|
+
|
|
42
|
+
Inside a tinted container the action uses **that container's text color**
|
|
43
|
+
(`danger.dark` on an error toast, `primary.dark` on an info toast). Never a
|
|
44
|
+
neutral control.
|
|
45
|
+
|
|
46
|
+
### 3. One action per toast; dismiss is ×
|
|
47
|
+
|
|
48
|
+
No 「닫기」 or 「취소」 button in a toast; the × already does that. If a
|
|
49
|
+
message needs two actions, it is not a toast; use a dialog or a pane.
|
|
50
|
+
|
|
51
|
+
### 4. Don't repeat the action in the copy
|
|
52
|
+
|
|
53
|
+
If the action says 「크레딧 구매」, the message doesn't also say "purchase
|
|
54
|
+
credits to continue". Toast copy stays **within 2 lines**.
|
|
55
|
+
|
|
56
|
+
### 5. In a pane, the fix is the primary
|
|
57
|
+
|
|
58
|
+
In a pane or empty state, the action that fixes the problem is the primary
|
|
59
|
+
(ink solid, `colorPalette="neutral" variant="solid"`). Dismiss is outline
|
|
60
|
+
neutral (`colorPalette="neutral" variant="outline"`).
|
|
61
|
+
|
|
62
|
+
### 6. Toasts with an action stay longer
|
|
63
|
+
|
|
64
|
+
The reader has to read the message and then decide to act. A toast with an
|
|
65
|
+
action defaults to **8s** instead of 5s. `useToast` does this for you when
|
|
66
|
+
`duration` is not given.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Which control goes where
|
|
71
|
+
|
|
72
|
+
| Surface | Primary action | Secondary / dismiss | Max actions | Copy |
|
|
73
|
+
| ------------------ | -------------------------------------------------- | -------------------------------- | ----------- | ------------------------------------------ |
|
|
74
|
+
| Toast | Text action in status color, via the `action` prop | × only | 1 | ≤ 2 lines, no hint that repeats the action |
|
|
75
|
+
| Inline banner | Trailing text action in status color | × if dismissible | 1 | 1 line, wraps on mobile |
|
|
76
|
+
| Pane / empty state | Ink solid (`neutral` `solid`) | Outline neutral | 2 | Title + reason + optional hint |
|
|
77
|
+
| Dialog | Ink solid, right | Outline neutral, left of primary | 2 | Title + body |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { useToast } from '@mindlogic-ai/logician-ui';
|
|
85
|
+
|
|
86
|
+
const showToast = useToast();
|
|
87
|
+
|
|
88
|
+
showToast({
|
|
89
|
+
status: 'error',
|
|
90
|
+
description: '크레딧이 부족해 응답을 생성하지 못했습니다.',
|
|
91
|
+
action: {
|
|
92
|
+
label: '크레딧 구매',
|
|
93
|
+
onClick: openCreditPurchaseModal,
|
|
94
|
+
},
|
|
95
|
+
// duration defaults to 8000 because an action is set
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- The action renders under the description, aligned with the text column, in
|
|
100
|
+
the toast's status color with the same hover tint as the ×.
|
|
101
|
+
- Clicking it calls `onClick` and dismisses the toast.
|
|
102
|
+
- `label` accepts any `ReactNode`, but keep it a short verb phrase.
|
|
103
|
+
- An explicit `duration` (including `null` for a persistent toast) always wins.
|
|
104
|
+
- Chakra's own `toaster.create({ action: { label, onClick } })` renders the
|
|
105
|
+
same way; with that route the label must be a string and the duration
|
|
106
|
+
default is Chakra's, not 8s.
|
|
107
|
+
|
|
108
|
+
### Don't: a button inside `description`
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// ✗ A neutral control dropped onto a tinted toast; it carries none of the
|
|
112
|
+
// toast's colors, adds a third line, and dismissal is left to the caller.
|
|
113
|
+
showToast({
|
|
114
|
+
status: 'error',
|
|
115
|
+
description: (
|
|
116
|
+
<>
|
|
117
|
+
크레딧이 부족합니다. 크레딧을 구매하면 계속 사용할 수 있습니다.
|
|
118
|
+
<Button colorPalette="neutral" variant="outline" onClick={buy}>
|
|
119
|
+
크레딧 구매
|
|
120
|
+
</Button>
|
|
121
|
+
</>
|
|
122
|
+
),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ✓ One action, in the toast's color, and the copy doesn't repeat it.
|
|
126
|
+
showToast({
|
|
127
|
+
status: 'error',
|
|
128
|
+
description: '크레딧이 부족합니다.',
|
|
129
|
+
action: { label: '크레딧 구매', onClick: buy },
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
See **Components/Toast → Action / …** for each status and a short-message case.
|