minka-ds 0.4.1 → 0.4.3
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/package.json
CHANGED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../lib/utils"
|
|
5
|
+
|
|
6
|
+
export type DiagramNodeVariant = "wallet" | "anchor"
|
|
7
|
+
|
|
8
|
+
/** Brand pair for state nodes (issue/destroy). */
|
|
9
|
+
export type DiagramAccent =
|
|
10
|
+
| "yellow-darkforest"
|
|
11
|
+
| "rose-coral"
|
|
12
|
+
| "blue-navy"
|
|
13
|
+
| "beige-bronze"
|
|
14
|
+
| "gray-black"
|
|
15
|
+
|
|
16
|
+
export interface DiagramNodeProps {
|
|
17
|
+
/** Node type — drives default colors. wallet = white/raised, anchor = inverted slate. */
|
|
18
|
+
variant?: DiagramNodeVariant
|
|
19
|
+
/** When false, renders a dashed empty slot. */
|
|
20
|
+
filled?: boolean
|
|
21
|
+
/** Pair-colored "state" node (issue/destroy). Overrides variant colors. */
|
|
22
|
+
accent?: DiagramAccent
|
|
23
|
+
/** Invert the accent fill (light bg + dark text). */
|
|
24
|
+
accentInverted?: boolean
|
|
25
|
+
/** Compact sizing (used by anchor/alias nodes). */
|
|
26
|
+
compact?: boolean
|
|
27
|
+
className?: string
|
|
28
|
+
children?: React.ReactNode
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// exact-dash rounded border for the empty state
|
|
32
|
+
function DashedBorder({ radius = 10 }: { radius?: number }) {
|
|
33
|
+
return (
|
|
34
|
+
<svg className="pointer-events-none absolute inset-0 size-full" aria-hidden>
|
|
35
|
+
<rect
|
|
36
|
+
x="0.75" y="0.75"
|
|
37
|
+
rx={radius} ry={radius}
|
|
38
|
+
fill="none"
|
|
39
|
+
stroke="var(--color-border-default)"
|
|
40
|
+
strokeWidth="1.5"
|
|
41
|
+
strokeDasharray="6 6"
|
|
42
|
+
style={{ width: "calc(100% - 1.5px)", height: "calc(100% - 1.5px)" }}
|
|
43
|
+
/>
|
|
44
|
+
</svg>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A node card for the diagram family (FlowDiagram, alias resolve, …). Raised
|
|
50
|
+
* two-layer treatment: a 4px stroke ring underneath + a surface on top whose
|
|
51
|
+
* lift shadow falls OVER the stroke (button-like). The stroke grows 0→4px as
|
|
52
|
+
* the node fills (smooth, no layout shift). Empty nodes render a dashed slot.
|
|
53
|
+
*
|
|
54
|
+
* Node type is encoded by color: wallet = white/raised, anchor = inverted slate,
|
|
55
|
+
* accent = pair-colored state (issue/destroy).
|
|
56
|
+
*/
|
|
57
|
+
function DiagramNode({
|
|
58
|
+
variant = "wallet",
|
|
59
|
+
filled = true,
|
|
60
|
+
accent,
|
|
61
|
+
accentInverted = false,
|
|
62
|
+
compact = false,
|
|
63
|
+
className,
|
|
64
|
+
children,
|
|
65
|
+
}: DiagramNodeProps) {
|
|
66
|
+
const sizeClass = compact ? "px-4 py-2 min-h-[48px] min-w-[140px]" : "px-5 py-3 min-h-[64px] min-w-[180px]"
|
|
67
|
+
|
|
68
|
+
// resolve fill / stroke / text per type
|
|
69
|
+
let fill = "var(--color-bg-raised)"
|
|
70
|
+
let stroke = "var(--color-border-default)"
|
|
71
|
+
let ink: string | undefined
|
|
72
|
+
|
|
73
|
+
if (accent) {
|
|
74
|
+
fill = accentInverted ? `var(--color-pair-${accent}-light)` : `var(--color-pair-${accent}-dark)`
|
|
75
|
+
stroke = accentInverted ? `var(--color-pair-${accent}-dark)` : `var(--color-pair-${accent}-light)`
|
|
76
|
+
ink = accentInverted ? "var(--color-text-default)" : "var(--color-text-inverse)"
|
|
77
|
+
} else if (variant === "anchor") {
|
|
78
|
+
fill = "var(--color-pair-blue-navy-dark)"
|
|
79
|
+
stroke = "var(--color-pair-blue-navy-light)"
|
|
80
|
+
ink = "var(--color-text-inverse)"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
data-slot="diagram-node"
|
|
86
|
+
className={cn("relative [border-radius:var(--radius-card)]", className)}
|
|
87
|
+
style={filled ? { animation: "diagram-node-pop .18s cubic-bezier(0.16,1,0.3,1) both" } : undefined}
|
|
88
|
+
>
|
|
89
|
+
<style>{`@keyframes diagram-node-pop { from { opacity:0; transform: scale(.96) } to { opacity:1; transform: scale(1) } }`}</style>
|
|
90
|
+
|
|
91
|
+
{/* bottom: stroke ring that grows 0→4px on fill */}
|
|
92
|
+
<div
|
|
93
|
+
className="absolute inset-0 [border-radius:var(--radius-card)] transition-[box-shadow] duration-300 ease-out"
|
|
94
|
+
style={{ boxShadow: filled ? `inset 0 0 0 4px ${stroke}` : "inset 0 0 0 0px transparent" }}
|
|
95
|
+
/>
|
|
96
|
+
|
|
97
|
+
{/* top: surface — inset so the ring shows; bg + lift fade in on fill */}
|
|
98
|
+
<div
|
|
99
|
+
className={cn(
|
|
100
|
+
"relative m-1 flex items-center justify-center [border-radius:calc(var(--radius-card)-4px)] text-center transition-[background-color,box-shadow] duration-300 ease-out",
|
|
101
|
+
sizeClass,
|
|
102
|
+
)}
|
|
103
|
+
style={{
|
|
104
|
+
backgroundColor: filled ? fill : "var(--color-bg-base)",
|
|
105
|
+
color: filled ? ink : undefined,
|
|
106
|
+
boxShadow: filled ? "var(--shadow-raised)" : "none",
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{!filled && <DashedBorder radius={6} />}
|
|
110
|
+
{children}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export { DiagramNode }
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
|
+
import { DiagramNode } from "./diagram-node"
|
|
5
|
+
|
|
6
|
+
/** Brand pair used to give a node / the flow a semantic accent. */
|
|
7
|
+
export type FlowAccent =
|
|
8
|
+
| "yellow-darkforest"
|
|
9
|
+
| "rose-coral"
|
|
10
|
+
| "blue-navy"
|
|
11
|
+
| "beige-bronze"
|
|
12
|
+
| "gray-black"
|
|
4
13
|
|
|
5
14
|
export interface FlowNode {
|
|
6
15
|
/** Node label (party name). */
|
|
@@ -13,6 +22,18 @@ export interface FlowNode {
|
|
|
13
22
|
subtitle?: string
|
|
14
23
|
/** True when this node is an unfilled slot (e.g. nothing selected yet). */
|
|
15
24
|
empty?: boolean
|
|
25
|
+
/** Optional leading icon (used by state nodes like issue/destroy). */
|
|
26
|
+
icon?: React.ReactNode
|
|
27
|
+
/**
|
|
28
|
+
* When set, this node is a "state" node (no balance) rendered in the pair's
|
|
29
|
+
* colors — used for the abstract create/destroy end of issue/destroy flows.
|
|
30
|
+
*/
|
|
31
|
+
accent?: FlowAccent
|
|
32
|
+
/**
|
|
33
|
+
* Invert the accent fill: light member as background, dark as text/border
|
|
34
|
+
* (instead of dark bg + light text). Used to contrast destroy vs issue.
|
|
35
|
+
*/
|
|
36
|
+
accentInverted?: boolean
|
|
16
37
|
}
|
|
17
38
|
|
|
18
39
|
type FormatFn = (n: number) => string
|
|
@@ -55,6 +76,11 @@ export interface FlowDiagramProps {
|
|
|
55
76
|
format?: FormatFn
|
|
56
77
|
/** Optional content rendered below the diagram (e.g. a reference pill). */
|
|
57
78
|
footer?: React.ReactNode
|
|
79
|
+
/**
|
|
80
|
+
* Semantic accent (brand pair) for the flow — tints the arrow, sheen and
|
|
81
|
+
* amount chip. Used by issue/destroy to read as generative/destructive.
|
|
82
|
+
*/
|
|
83
|
+
accent?: FlowAccent
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
/**
|
|
@@ -65,12 +91,12 @@ export interface FlowDiagramProps {
|
|
|
65
91
|
* an amount is entered. Unfilled nodes render as a dashed empty slot. Neutral
|
|
66
92
|
* styling — suited to neutral transfers.
|
|
67
93
|
*/
|
|
68
|
-
function FlowDiagram({ top, bottom, amount, direction, currency = "COP", format = defaultFormat, footer }: FlowDiagramProps) {
|
|
94
|
+
function FlowDiagram({ top, bottom, amount, direction, currency = "COP", format = defaultFormat, footer, accent }: FlowDiagramProps) {
|
|
69
95
|
const active = amount > 0
|
|
70
96
|
const glintKey = active ? amount : 0
|
|
71
97
|
|
|
72
98
|
return (
|
|
73
|
-
<div data-slot="flow-diagram" className="flex flex-col items-center">
|
|
99
|
+
<div data-slot="flow-diagram" className="relative flex flex-col items-center">
|
|
74
100
|
<style>{`
|
|
75
101
|
@keyframes flow-slide-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
|
76
102
|
@keyframes flow-pop { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
|
|
@@ -78,11 +104,15 @@ function FlowDiagram({ top, bottom, amount, direction, currency = "COP", format
|
|
|
78
104
|
|
|
79
105
|
<FlowNodeCard key={top.empty ? "top-empty" : `top-${top.name}`} node={top} active={active} format={format} pop />
|
|
80
106
|
|
|
81
|
-
<Connector active={active} direction={direction} amount={amount} currency={currency} format={format} glintKey={glintKey} />
|
|
107
|
+
<Connector active={active} direction={direction} amount={amount} currency={currency} format={format} glintKey={glintKey} accent={accent} />
|
|
82
108
|
|
|
83
109
|
<FlowNodeCard key={bottom.empty ? "bottom-empty" : `bottom-${bottom.name}`} node={bottom} active={active} format={format} pop />
|
|
84
110
|
|
|
85
|
-
{footer
|
|
111
|
+
{/* footer (e.g. reference pill) is absolutely positioned above the diagram
|
|
112
|
+
so it doesn't shift the diagram's vertical centering when it appears */}
|
|
113
|
+
{footer && (
|
|
114
|
+
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-6">{footer}</div>
|
|
115
|
+
)}
|
|
86
116
|
</div>
|
|
87
117
|
)
|
|
88
118
|
}
|
|
@@ -91,7 +121,7 @@ function FlowDiagram({ top, bottom, amount, direction, currency = "COP", format
|
|
|
91
121
|
const CONNECTOR_H = 80
|
|
92
122
|
|
|
93
123
|
function Connector({
|
|
94
|
-
active, direction, amount, currency, format, glintKey,
|
|
124
|
+
active, direction, amount, currency, format, glintKey, accent,
|
|
95
125
|
}: {
|
|
96
126
|
active: boolean
|
|
97
127
|
direction: "up" | "down"
|
|
@@ -99,11 +129,15 @@ function Connector({
|
|
|
99
129
|
currency: string
|
|
100
130
|
format: FormatFn
|
|
101
131
|
glintKey: number
|
|
132
|
+
accent?: FlowAccent
|
|
102
133
|
}) {
|
|
103
134
|
const gid = React.useId()
|
|
104
135
|
const H = CONNECTOR_H
|
|
105
136
|
const headUp = direction === "up"
|
|
106
137
|
|
|
138
|
+
// active stroke: semantic pair-dark when accented, else default ink
|
|
139
|
+
const activeStroke = accent ? `var(--color-pair-${accent}-dark)` : "var(--color-text-default)"
|
|
140
|
+
|
|
107
141
|
const head = headUp ? "M2 6 L7 0 L12 6" : `M2 ${H - 6} L7 ${H} L12 ${H - 6}`
|
|
108
142
|
const shapes = (
|
|
109
143
|
<>
|
|
@@ -115,7 +149,7 @@ function Connector({
|
|
|
115
149
|
return (
|
|
116
150
|
<div className="relative flex items-center justify-center my-1" style={{ height: H }}>
|
|
117
151
|
<svg width="14" height={H} viewBox={`0 0 14 ${H}`} className="block overflow-visible" aria-hidden>
|
|
118
|
-
<g stroke={active ?
|
|
152
|
+
<g stroke={active ? activeStroke : "var(--color-border-strong)"} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none">
|
|
119
153
|
{shapes}
|
|
120
154
|
</g>
|
|
121
155
|
{active && (
|
|
@@ -152,50 +186,66 @@ function Connector({
|
|
|
152
186
|
)
|
|
153
187
|
}
|
|
154
188
|
|
|
155
|
-
function FlowNodeCard({ node, active, format
|
|
189
|
+
function FlowNodeCard({ node, active, format }: { node: FlowNode; active: boolean; format: FormatFn; pop?: boolean }) {
|
|
156
190
|
const showAfter = active && node.after !== null
|
|
157
191
|
const overdrawn = node.after !== null && node.after < 0
|
|
158
192
|
|
|
193
|
+
// Empty slot
|
|
159
194
|
if (node.empty) {
|
|
160
195
|
return (
|
|
161
|
-
<
|
|
162
|
-
<DashedBorder radius={10} />
|
|
196
|
+
<DiagramNode filled={false}>
|
|
163
197
|
<span className="text-body-sm text-[var(--color-text-hint)]">{node.name}</span>
|
|
164
|
-
</
|
|
198
|
+
</DiagramNode>
|
|
165
199
|
)
|
|
166
200
|
}
|
|
167
201
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
) : (
|
|
178
|
-
<div className="flex flex-col items-center leading-none">
|
|
179
|
-
<span
|
|
180
|
-
className={`origin-bottom transition-all duration-300 ${
|
|
181
|
-
showAfter
|
|
182
|
-
? "scale-[0.78] text-caption text-[var(--color-text-muted)] mb-0.5"
|
|
183
|
-
: "text-label-mono text-[var(--color-text-default)]"
|
|
184
|
-
}`}
|
|
185
|
-
>
|
|
186
|
-
{showAfter ? `was ${format(node.current)}` : format(node.current)}
|
|
202
|
+
// State node (issue/destroy): pair-colored, no balance. Icon + label, subtext below.
|
|
203
|
+
if (node.accent) {
|
|
204
|
+
const subInk = node.accentInverted ? "var(--color-text-default)" : "var(--color-text-inverse)"
|
|
205
|
+
return (
|
|
206
|
+
<DiagramNode accent={node.accent} accentInverted={node.accentInverted}>
|
|
207
|
+
<div className="flex flex-col items-center gap-1">
|
|
208
|
+
<span className="flex items-center gap-1.5 text-body-sm">
|
|
209
|
+
{node.icon && <span className="flex items-center [&_svg]:size-4">{node.icon}</span>}
|
|
210
|
+
{node.name}
|
|
187
211
|
</span>
|
|
188
|
-
{
|
|
212
|
+
{node.subtitle && <span className="text-caption" style={{ color: subInk }}>{node.subtitle}</span>}
|
|
213
|
+
</div>
|
|
214
|
+
</DiagramNode>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Wallet / balance node
|
|
219
|
+
return (
|
|
220
|
+
<DiagramNode variant="wallet">
|
|
221
|
+
<div className="flex flex-col items-center gap-1">
|
|
222
|
+
<span className="text-body-sm text-[var(--color-text-default)]">{node.name}</span>
|
|
223
|
+
|
|
224
|
+
{node.current === null ? (
|
|
225
|
+
node.subtitle ? <span className="text-caption text-[var(--color-text-muted)]">{node.subtitle}</span> : null
|
|
226
|
+
) : (
|
|
227
|
+
<div className="flex flex-col items-center leading-none">
|
|
189
228
|
<span
|
|
190
|
-
className={`
|
|
191
|
-
|
|
229
|
+
className={`origin-bottom transition-all duration-300 ${
|
|
230
|
+
showAfter
|
|
231
|
+
? "scale-[0.78] text-caption text-[var(--color-text-muted)] mb-0.5"
|
|
232
|
+
: "text-label-mono text-[var(--color-text-default)]"
|
|
233
|
+
}`}
|
|
192
234
|
>
|
|
193
|
-
{format(node.
|
|
235
|
+
{showAfter ? `was ${format(node.current)}` : format(node.current)}
|
|
194
236
|
</span>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
237
|
+
{showAfter && (
|
|
238
|
+
<span
|
|
239
|
+
className={`text-label-mono ${overdrawn ? "text-[var(--color-feedback-error)]" : "text-[var(--color-text-default)]"}`}
|
|
240
|
+
style={{ animation: "flow-slide-in .3s ease both" }}
|
|
241
|
+
>
|
|
242
|
+
{format(node.after!)}
|
|
243
|
+
</span>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
</DiagramNode>
|
|
199
249
|
)
|
|
200
250
|
}
|
|
201
251
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Popover as PopoverPrimitive } from "radix-ui"
|
|
5
|
+
import { HelpCircle, X, ExternalLink } from "lucide-react"
|
|
6
|
+
import { cn } from "../../lib/utils"
|
|
7
|
+
|
|
8
|
+
type Anchor = "bottom-right" | "bottom-left" | "top-right" | "top-left"
|
|
9
|
+
|
|
10
|
+
export interface HelpExpanderProps {
|
|
11
|
+
/** Card heading. */
|
|
12
|
+
title: string
|
|
13
|
+
/** Card body (text or nodes). */
|
|
14
|
+
children: React.ReactNode
|
|
15
|
+
/**
|
|
16
|
+
* "popover" (default) floats the card next to the trigger (portaled — works
|
|
17
|
+
* anywhere). "inset" expands the card inside the nearest positioned container,
|
|
18
|
+
* frosting whatever is behind it — for panels with empty space to fill.
|
|
19
|
+
*/
|
|
20
|
+
mode?: "popover" | "inset"
|
|
21
|
+
/** Trigger appearance. Defaults to a circular "?" icon button, no label. */
|
|
22
|
+
trigger?: { icon?: React.ReactNode; label?: string }
|
|
23
|
+
/** inset mode: which corner the button sits in / the card expands from. */
|
|
24
|
+
anchor?: Anchor
|
|
25
|
+
/** Optional doc link rendered under the body. */
|
|
26
|
+
docHref?: string
|
|
27
|
+
docLabel?: string
|
|
28
|
+
className?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ANCHOR_POS: Record<Anchor, string> = {
|
|
32
|
+
"bottom-right": "bottom-3 right-3 items-end",
|
|
33
|
+
"bottom-left": "bottom-3 left-3 items-start",
|
|
34
|
+
"top-right": "top-3 right-3 items-end",
|
|
35
|
+
"top-left": "top-3 left-3 items-start",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// shared trigger button
|
|
39
|
+
function TriggerButton({ trigger, ...props }: { trigger?: HelpExpanderProps["trigger"] } & React.ComponentProps<"button">) {
|
|
40
|
+
const icon = trigger?.icon ?? <HelpCircle className="size-4" />
|
|
41
|
+
return (
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
aria-label="Help"
|
|
45
|
+
className={cn(
|
|
46
|
+
"flex items-center justify-center gap-1.5 [border-radius:var(--radius-button)] border border-[var(--color-border-default)] bg-[var(--color-bg-raised)] text-[var(--color-text-muted)] shadow-[var(--shadow-card)] transition-colors hover:text-[var(--color-text-default)] hover:border-[var(--color-border-strong)]",
|
|
47
|
+
trigger?.label ? "h-8 px-3 text-label-sm" : "size-8",
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
{icon}
|
|
52
|
+
{trigger?.label}
|
|
53
|
+
</button>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// shared frosted card body
|
|
58
|
+
function CardBody({ title, children, docHref, docLabel, onClose }: {
|
|
59
|
+
title: string
|
|
60
|
+
children: React.ReactNode
|
|
61
|
+
docHref?: string
|
|
62
|
+
docLabel?: string
|
|
63
|
+
onClose: () => void
|
|
64
|
+
}) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
className="w-full [border-radius:var(--radius-card)] border border-[var(--color-border-default)] backdrop-blur-md shadow-[var(--shadow-popover)] p-4 [animation:help-in_.2s_cubic-bezier(0.16,1,0.3,1)]"
|
|
68
|
+
style={{ backgroundColor: "color-mix(in srgb, var(--color-bg-overlay) 70%, transparent)" }}
|
|
69
|
+
>
|
|
70
|
+
<style>{`@keyframes help-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }`}</style>
|
|
71
|
+
<div className="flex items-start justify-between gap-3">
|
|
72
|
+
<span className="text-heading-4-serif text-[var(--color-text-default)]">{title}</span>
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
aria-label="Close help"
|
|
76
|
+
onClick={onClose}
|
|
77
|
+
className="shrink-0 -mr-1 -mt-1 size-6 flex items-center justify-center rounded-[var(--radius-button)] text-[var(--color-text-muted)] hover:bg-[var(--color-action-ghost-hover)] hover:text-[var(--color-text-default)] transition-colors"
|
|
78
|
+
>
|
|
79
|
+
<X className="size-3.5" />
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="mt-1.5 text-caption text-[var(--color-text-muted)] leading-relaxed">{children}</div>
|
|
83
|
+
{docHref && (
|
|
84
|
+
<a
|
|
85
|
+
href={docHref}
|
|
86
|
+
target="_blank"
|
|
87
|
+
rel="noreferrer"
|
|
88
|
+
className="mt-2.5 inline-flex items-center gap-1 text-caption text-[var(--color-text-link)] hover:text-[var(--color-text-link-hover)]"
|
|
89
|
+
>
|
|
90
|
+
{docLabel ?? "Learn more"}
|
|
91
|
+
<ExternalLink className="size-3" />
|
|
92
|
+
</a>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function HelpExpander({
|
|
99
|
+
title, children, mode = "popover", trigger, anchor = "bottom-right", docHref, docLabel, className,
|
|
100
|
+
}: HelpExpanderProps) {
|
|
101
|
+
const [open, setOpen] = React.useState(false)
|
|
102
|
+
|
|
103
|
+
// ── inset: expands inside the nearest positioned container ──
|
|
104
|
+
if (mode === "inset") {
|
|
105
|
+
return (
|
|
106
|
+
<div className={cn("absolute z-10 flex flex-col w-[calc(100%-1.5rem)]", ANCHOR_POS[anchor], className)}>
|
|
107
|
+
{open ? (
|
|
108
|
+
<CardBody title={title} docHref={docHref} docLabel={docLabel} onClose={() => setOpen(false)}>
|
|
109
|
+
{children}
|
|
110
|
+
</CardBody>
|
|
111
|
+
) : (
|
|
112
|
+
<TriggerButton trigger={trigger} onClick={() => setOpen(true)} />
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── popover: floats next to the trigger (portaled) ──
|
|
119
|
+
return (
|
|
120
|
+
<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
|
|
121
|
+
<PopoverPrimitive.Trigger asChild>
|
|
122
|
+
<TriggerButton trigger={trigger} className={className} />
|
|
123
|
+
</PopoverPrimitive.Trigger>
|
|
124
|
+
<PopoverPrimitive.Portal>
|
|
125
|
+
<PopoverPrimitive.Content
|
|
126
|
+
side="top"
|
|
127
|
+
align="end"
|
|
128
|
+
sideOffset={8}
|
|
129
|
+
className="z-[var(--z-popover)] w-72 outline-none"
|
|
130
|
+
>
|
|
131
|
+
<CardBody title={title} docHref={docHref} docLabel={docLabel} onClose={() => setOpen(false)}>
|
|
132
|
+
{children}
|
|
133
|
+
</CardBody>
|
|
134
|
+
</PopoverPrimitive.Content>
|
|
135
|
+
</PopoverPrimitive.Portal>
|
|
136
|
+
</PopoverPrimitive.Root>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { HelpExpander }
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,8 @@ export * from "./components/ui/tabs"
|
|
|
44
44
|
export * from "./components/ui/textarea"
|
|
45
45
|
export * from "./components/ui/tooltip"
|
|
46
46
|
export * from "./components/ui/flow-diagram"
|
|
47
|
+
export * from "./components/ui/diagram-node"
|
|
48
|
+
export * from "./components/ui/help-expander"
|
|
47
49
|
|
|
48
50
|
// Brand textures + logo
|
|
49
51
|
export * from "./textures"
|
package/tokens/primitives.css
CHANGED
|
@@ -240,6 +240,7 @@
|
|
|
240
240
|
--primitive-shadow-md: 0 4px 6px oklch(0 0 0 / 0.07), 0 2px 4px oklch(0 0 0 / 0.06);
|
|
241
241
|
--primitive-shadow-lg: 0 10px 15px oklch(0 0 0 / 0.10), 0 4px 6px oklch(0 0 0 / 0.05);
|
|
242
242
|
--primitive-shadow-xl: 0 20px 25px oklch(0 0 0 / 0.10), 0 8px 10px oklch(0 0 0 / 0.04);
|
|
243
|
+
--primitive-shadow-raised: 0 4px 12px -1px oklch(0 0 0 / 0.18), 0 2px 5px -1px oklch(0 0 0 / 0.12);
|
|
243
244
|
|
|
244
245
|
/* --- Z-index scale --- */
|
|
245
246
|
--primitive-z-base: 0;
|
package/tokens/semantic.css
CHANGED
|
@@ -192,6 +192,7 @@
|
|
|
192
192
|
--shadow-card: var(--primitive-shadow-xs);
|
|
193
193
|
--shadow-popover: var(--primitive-shadow-md);
|
|
194
194
|
--shadow-modal: var(--primitive-shadow-lg);
|
|
195
|
+
--shadow-raised: var(--primitive-shadow-raised);
|
|
195
196
|
}
|
|
196
197
|
|
|
197
198
|
/* ── Dark mode ───────────────────────────────────────────────────── */
|
|
@@ -321,4 +322,5 @@
|
|
|
321
322
|
--shadow-card: none;
|
|
322
323
|
--shadow-popover: var(--primitive-shadow-lg);
|
|
323
324
|
--shadow-modal: var(--primitive-shadow-xl);
|
|
325
|
+
--shadow-raised: var(--primitive-shadow-raised);
|
|
324
326
|
}
|