minka-ds 0.4.1 → 0.4.2
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
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
4
|
|
|
5
|
+
/** Brand pair used to give a node / the flow a semantic accent. */
|
|
6
|
+
export type FlowAccent =
|
|
7
|
+
| "yellow-darkforest"
|
|
8
|
+
| "rose-coral"
|
|
9
|
+
| "blue-navy"
|
|
10
|
+
| "beige-bronze"
|
|
11
|
+
| "gray-black"
|
|
12
|
+
|
|
5
13
|
export interface FlowNode {
|
|
6
14
|
/** Node label (party name). */
|
|
7
15
|
name: string
|
|
@@ -13,6 +21,18 @@ export interface FlowNode {
|
|
|
13
21
|
subtitle?: string
|
|
14
22
|
/** True when this node is an unfilled slot (e.g. nothing selected yet). */
|
|
15
23
|
empty?: boolean
|
|
24
|
+
/** Optional leading icon (used by state nodes like issue/destroy). */
|
|
25
|
+
icon?: React.ReactNode
|
|
26
|
+
/**
|
|
27
|
+
* When set, this node is a "state" node (no balance) rendered in the pair's
|
|
28
|
+
* colors — used for the abstract create/destroy end of issue/destroy flows.
|
|
29
|
+
*/
|
|
30
|
+
accent?: FlowAccent
|
|
31
|
+
/**
|
|
32
|
+
* Invert the accent fill: light member as background, dark as text/border
|
|
33
|
+
* (instead of dark bg + light text). Used to contrast destroy vs issue.
|
|
34
|
+
*/
|
|
35
|
+
accentInverted?: boolean
|
|
16
36
|
}
|
|
17
37
|
|
|
18
38
|
type FormatFn = (n: number) => string
|
|
@@ -55,6 +75,11 @@ export interface FlowDiagramProps {
|
|
|
55
75
|
format?: FormatFn
|
|
56
76
|
/** Optional content rendered below the diagram (e.g. a reference pill). */
|
|
57
77
|
footer?: React.ReactNode
|
|
78
|
+
/**
|
|
79
|
+
* Semantic accent (brand pair) for the flow — tints the arrow, sheen and
|
|
80
|
+
* amount chip. Used by issue/destroy to read as generative/destructive.
|
|
81
|
+
*/
|
|
82
|
+
accent?: FlowAccent
|
|
58
83
|
}
|
|
59
84
|
|
|
60
85
|
/**
|
|
@@ -65,12 +90,12 @@ export interface FlowDiagramProps {
|
|
|
65
90
|
* an amount is entered. Unfilled nodes render as a dashed empty slot. Neutral
|
|
66
91
|
* styling — suited to neutral transfers.
|
|
67
92
|
*/
|
|
68
|
-
function FlowDiagram({ top, bottom, amount, direction, currency = "COP", format = defaultFormat, footer }: FlowDiagramProps) {
|
|
93
|
+
function FlowDiagram({ top, bottom, amount, direction, currency = "COP", format = defaultFormat, footer, accent }: FlowDiagramProps) {
|
|
69
94
|
const active = amount > 0
|
|
70
95
|
const glintKey = active ? amount : 0
|
|
71
96
|
|
|
72
97
|
return (
|
|
73
|
-
<div data-slot="flow-diagram" className="flex flex-col items-center">
|
|
98
|
+
<div data-slot="flow-diagram" className="relative flex flex-col items-center">
|
|
74
99
|
<style>{`
|
|
75
100
|
@keyframes flow-slide-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
|
76
101
|
@keyframes flow-pop { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
|
|
@@ -78,11 +103,15 @@ function FlowDiagram({ top, bottom, amount, direction, currency = "COP", format
|
|
|
78
103
|
|
|
79
104
|
<FlowNodeCard key={top.empty ? "top-empty" : `top-${top.name}`} node={top} active={active} format={format} pop />
|
|
80
105
|
|
|
81
|
-
<Connector active={active} direction={direction} amount={amount} currency={currency} format={format} glintKey={glintKey} />
|
|
106
|
+
<Connector active={active} direction={direction} amount={amount} currency={currency} format={format} glintKey={glintKey} accent={accent} />
|
|
82
107
|
|
|
83
108
|
<FlowNodeCard key={bottom.empty ? "bottom-empty" : `bottom-${bottom.name}`} node={bottom} active={active} format={format} pop />
|
|
84
109
|
|
|
85
|
-
{footer
|
|
110
|
+
{/* footer (e.g. reference pill) is absolutely positioned above the diagram
|
|
111
|
+
so it doesn't shift the diagram's vertical centering when it appears */}
|
|
112
|
+
{footer && (
|
|
113
|
+
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-6">{footer}</div>
|
|
114
|
+
)}
|
|
86
115
|
</div>
|
|
87
116
|
)
|
|
88
117
|
}
|
|
@@ -91,7 +120,7 @@ function FlowDiagram({ top, bottom, amount, direction, currency = "COP", format
|
|
|
91
120
|
const CONNECTOR_H = 80
|
|
92
121
|
|
|
93
122
|
function Connector({
|
|
94
|
-
active, direction, amount, currency, format, glintKey,
|
|
123
|
+
active, direction, amount, currency, format, glintKey, accent,
|
|
95
124
|
}: {
|
|
96
125
|
active: boolean
|
|
97
126
|
direction: "up" | "down"
|
|
@@ -99,11 +128,15 @@ function Connector({
|
|
|
99
128
|
currency: string
|
|
100
129
|
format: FormatFn
|
|
101
130
|
glintKey: number
|
|
131
|
+
accent?: FlowAccent
|
|
102
132
|
}) {
|
|
103
133
|
const gid = React.useId()
|
|
104
134
|
const H = CONNECTOR_H
|
|
105
135
|
const headUp = direction === "up"
|
|
106
136
|
|
|
137
|
+
// active stroke: semantic pair-dark when accented, else default ink
|
|
138
|
+
const activeStroke = accent ? `var(--color-pair-${accent}-dark)` : "var(--color-text-default)"
|
|
139
|
+
|
|
107
140
|
const head = headUp ? "M2 6 L7 0 L12 6" : `M2 ${H - 6} L7 ${H} L12 ${H - 6}`
|
|
108
141
|
const shapes = (
|
|
109
142
|
<>
|
|
@@ -115,7 +148,7 @@ function Connector({
|
|
|
115
148
|
return (
|
|
116
149
|
<div className="relative flex items-center justify-center my-1" style={{ height: H }}>
|
|
117
150
|
<svg width="14" height={H} viewBox={`0 0 14 ${H}`} className="block overflow-visible" aria-hidden>
|
|
118
|
-
<g stroke={active ?
|
|
151
|
+
<g stroke={active ? activeStroke : "var(--color-border-strong)"} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none">
|
|
119
152
|
{shapes}
|
|
120
153
|
</g>
|
|
121
154
|
{active && (
|
|
@@ -165,6 +198,28 @@ function FlowNodeCard({ node, active, format, pop }: { node: FlowNode; active: b
|
|
|
165
198
|
)
|
|
166
199
|
}
|
|
167
200
|
|
|
201
|
+
// State node (issue/destroy abstract end): pair-colored, no balance. Icon sits
|
|
202
|
+
// inline next to the main label; subtext below.
|
|
203
|
+
if (node.accent) {
|
|
204
|
+
// default: dark fill + light text; inverted: light fill + dark text.
|
|
205
|
+
const fill = node.accentInverted ? `var(--color-pair-${node.accent}-light)` : `var(--color-pair-${node.accent}-dark)`
|
|
206
|
+
const ink = node.accentInverted ? `var(--color-pair-${node.accent}-dark)` : `var(--color-pair-${node.accent}-light)`
|
|
207
|
+
const ring = node.accentInverted ? `var(--color-pair-${node.accent}-dark)` : `var(--color-pair-${node.accent}-light)`
|
|
208
|
+
const subInk = node.accentInverted ? "var(--color-text-default)" : "var(--color-text-inverse)"
|
|
209
|
+
return (
|
|
210
|
+
<div
|
|
211
|
+
className="flex flex-col items-center gap-1 [border-radius:var(--radius-card)] border px-5 py-3 text-center min-w-[180px]"
|
|
212
|
+
style={{ backgroundColor: fill, color: ink, borderColor: ring }}
|
|
213
|
+
>
|
|
214
|
+
<span className="flex items-center gap-1.5 text-body-sm">
|
|
215
|
+
{node.icon && <span className="flex items-center [&_svg]:size-4">{node.icon}</span>}
|
|
216
|
+
{node.name}
|
|
217
|
+
</span>
|
|
218
|
+
{node.subtitle && <span className="text-caption" style={{ color: subInk }}>{node.subtitle}</span>}
|
|
219
|
+
</div>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
168
223
|
return (
|
|
169
224
|
<div
|
|
170
225
|
className="flex flex-col items-center gap-1 [border-radius:var(--radius-card)] border border-[var(--color-border-default)] bg-[var(--color-bg-raised)] px-5 py-3 text-center min-w-[180px]"
|
|
@@ -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,7 @@ 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/help-expander"
|
|
47
48
|
|
|
48
49
|
// Brand textures + logo
|
|
49
50
|
export * from "./textures"
|