@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.
Files changed (98) hide show
  1. package/USAGE.md +17 -0
  2. package/dist/components/Form/FormSection/CollapsibleFormSection.js +1 -0
  3. package/dist/components/Form/FormSection/CollapsibleFormSection.js.map +1 -1
  4. package/dist/components/Form/FormSection/CollapsibleFormSection.mjs +1 -0
  5. package/dist/components/Form/FormSection/CollapsibleFormSection.mjs.map +1 -1
  6. package/dist/components/Toast/Toast.d.ts +7 -3
  7. package/dist/components/Toast/Toast.d.ts.map +1 -1
  8. package/dist/components/Toast/Toast.js +7 -3
  9. package/dist/components/Toast/Toast.js.map +1 -1
  10. package/dist/components/Toast/Toast.mjs +8 -4
  11. package/dist/components/Toast/Toast.mjs.map +1 -1
  12. package/dist/components/Toast/Toast.styles.d.ts +8 -0
  13. package/dist/components/Toast/Toast.styles.d.ts.map +1 -1
  14. package/dist/components/Toast/Toast.styles.js +59 -0
  15. package/dist/components/Toast/Toast.styles.js.map +1 -1
  16. package/dist/components/Toast/Toast.styles.mjs +57 -1
  17. package/dist/components/Toast/Toast.styles.mjs.map +1 -1
  18. package/dist/components/Toast/Toast.types.d.ts +25 -1
  19. package/dist/components/Toast/Toast.types.d.ts.map +1 -1
  20. package/dist/components/Toast/Toaster.d.ts.map +1 -1
  21. package/dist/components/Toast/Toaster.js +9 -1
  22. package/dist/components/Toast/Toaster.js.map +1 -1
  23. package/dist/components/Toast/Toaster.mjs +9 -1
  24. package/dist/components/Toast/Toaster.mjs.map +1 -1
  25. package/dist/components/Toast/useToast.d.ts +1 -1
  26. package/dist/components/Toast/useToast.d.ts.map +1 -1
  27. package/dist/components/Toast/useToast.js +9 -1
  28. package/dist/components/Toast/useToast.js.map +1 -1
  29. package/dist/components/Toast/useToast.mjs +10 -2
  30. package/dist/components/Toast/useToast.mjs.map +1 -1
  31. package/dist/components/Workflow/Workflow.d.ts +1 -1
  32. package/dist/components/Workflow/Workflow.d.ts.map +1 -1
  33. package/dist/components/Workflow/Workflow.js +2 -2
  34. package/dist/components/Workflow/Workflow.js.map +1 -1
  35. package/dist/components/Workflow/Workflow.mjs +2 -2
  36. package/dist/components/Workflow/Workflow.mjs.map +1 -1
  37. package/dist/components/Workflow/Workflow.types.d.ts +9 -0
  38. package/dist/components/Workflow/Workflow.types.d.ts.map +1 -1
  39. package/dist/components/Workflow/Workflow.types.js.map +1 -1
  40. package/dist/components/Workflow/Workflow.types.mjs.map +1 -1
  41. package/dist/components/Workflow/canvas/Canvas/Canvas.d.ts +6 -2
  42. package/dist/components/Workflow/canvas/Canvas/Canvas.d.ts.map +1 -1
  43. package/dist/components/Workflow/canvas/Canvas/Canvas.js +29 -23
  44. package/dist/components/Workflow/canvas/Canvas/Canvas.js.map +1 -1
  45. package/dist/components/Workflow/canvas/Canvas/Canvas.mjs +31 -25
  46. package/dist/components/Workflow/canvas/Canvas/Canvas.mjs.map +1 -1
  47. package/dist/components/Workflow/createNode.d.ts +25 -1
  48. package/dist/components/Workflow/createNode.d.ts.map +1 -1
  49. package/dist/components/Workflow/createNode.js +39 -1
  50. package/dist/components/Workflow/createNode.js.map +1 -1
  51. package/dist/components/Workflow/createNode.mjs +38 -2
  52. package/dist/components/Workflow/createNode.mjs.map +1 -1
  53. package/dist/components/Workflow/index.d.ts +2 -0
  54. package/dist/components/Workflow/index.d.ts.map +1 -1
  55. package/dist/components/Workflow/layout/autoLayout.d.ts +10 -0
  56. package/dist/components/Workflow/layout/autoLayout.d.ts.map +1 -1
  57. package/dist/components/Workflow/layout/autoLayout.js +4 -0
  58. package/dist/components/Workflow/layout/autoLayout.js.map +1 -1
  59. package/dist/components/Workflow/layout/autoLayout.mjs +1 -1
  60. package/dist/components/Workflow/layout/autoLayout.mjs.map +1 -1
  61. package/dist/components/Workflow/placement.d.ts +65 -0
  62. package/dist/components/Workflow/placement.d.ts.map +1 -0
  63. package/dist/components/Workflow/placement.js +100 -0
  64. package/dist/components/Workflow/placement.js.map +1 -0
  65. package/dist/components/Workflow/placement.mjs +96 -0
  66. package/dist/components/Workflow/placement.mjs.map +1 -0
  67. package/dist/components/Workflow/useWorkflowActions.d.ts +54 -0
  68. package/dist/components/Workflow/useWorkflowActions.d.ts.map +1 -0
  69. package/dist/components/Workflow/useWorkflowActions.js +105 -0
  70. package/dist/components/Workflow/useWorkflowActions.js.map +1 -0
  71. package/dist/components/Workflow/useWorkflowActions.mjs +103 -0
  72. package/dist/components/Workflow/useWorkflowActions.mjs.map +1 -0
  73. package/dist/index.js +5 -0
  74. package/dist/index.js.map +1 -1
  75. package/dist/index.mjs +2 -1
  76. package/dist/index.mjs.map +1 -1
  77. package/package.json +1 -1
  78. package/src/components/Toast/Toast.action.test.tsx +135 -0
  79. package/src/components/Toast/Toast.stories.tsx +111 -0
  80. package/src/components/Toast/Toast.styles.ts +60 -0
  81. package/src/components/Toast/Toast.tsx +16 -3
  82. package/src/components/Toast/Toast.types.ts +26 -1
  83. package/src/components/Toast/Toaster.tsx +11 -0
  84. package/src/components/Toast/useToast.tsx +17 -2
  85. package/src/components/Workflow/README.md +64 -0
  86. package/src/components/Workflow/Workflow.tsx +2 -0
  87. package/src/components/Workflow/Workflow.types.ts +10 -0
  88. package/src/components/Workflow/canvas/Canvas/Canvas.tsx +57 -53
  89. package/src/components/Workflow/createNode.test.ts +40 -2
  90. package/src/components/Workflow/createNode.ts +70 -2
  91. package/src/components/Workflow/index.ts +8 -0
  92. package/src/components/Workflow/layout/autoLayout.ts +4 -4
  93. package/src/components/Workflow/placement.test.ts +156 -0
  94. package/src/components/Workflow/placement.ts +172 -0
  95. package/src/components/Workflow/stories/Workflow.stories.tsx +79 -0
  96. package/src/components/Workflow/useWorkflowActions.test.tsx +345 -0
  97. package/src/components/Workflow/useWorkflowActions.ts +196 -0
  98. 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.