minka-ds 0.4.0 → 0.4.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.
package/package.json
CHANGED
|
@@ -17,6 +17,8 @@ interface AvatarProps {
|
|
|
17
17
|
size?: "sm" | "md" | "lg"
|
|
18
18
|
/** Background for the initials state. Defaults to a brand color. */
|
|
19
19
|
background?: string
|
|
20
|
+
/** Foreground (initials) color. Defaults to the inverse text token. */
|
|
21
|
+
color?: string
|
|
20
22
|
className?: string
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -25,7 +27,7 @@ function deriveInitials(name?: string): string {
|
|
|
25
27
|
return name.trim().split(/\s+/).map(p => p[0]).join("").slice(0, 2).toUpperCase()
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
function Avatar({ src, name, initials, size = "md", background, className }: AvatarProps) {
|
|
30
|
+
function Avatar({ src, name, initials, size = "md", background, color, className }: AvatarProps) {
|
|
29
31
|
return (
|
|
30
32
|
<div
|
|
31
33
|
data-slot="avatar"
|
|
@@ -34,7 +36,10 @@ function Avatar({ src, name, initials, size = "md", background, className }: Ava
|
|
|
34
36
|
SIZE[size],
|
|
35
37
|
className
|
|
36
38
|
)}
|
|
37
|
-
style={{
|
|
39
|
+
style={{
|
|
40
|
+
background: src ? undefined : (background ?? "var(--color-brand-blue)"),
|
|
41
|
+
...(color ? { color } : {}),
|
|
42
|
+
}}
|
|
38
43
|
>
|
|
39
44
|
{src
|
|
40
45
|
? <img src={src} alt={name ?? ""} className="size-full object-cover" />
|
|
@@ -47,6 +47,44 @@ function DialogOverlay({
|
|
|
47
47
|
)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
type PanelPlacement = "side" | "top"
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Optional contextual panel rendered inside DialogContent. Holds supporting
|
|
54
|
+
* media or guidance (illustration, brand texture, action summary, help text).
|
|
55
|
+
* Place it as a direct child of DialogContent; the content adapts its layout.
|
|
56
|
+
*
|
|
57
|
+
* By default the panel bleeds to the dialog edges. Pass `inset` to float it
|
|
58
|
+
* inside the dialog with an 8px frame and rounded corners.
|
|
59
|
+
*/
|
|
60
|
+
function DialogPanel({
|
|
61
|
+
className,
|
|
62
|
+
placement = "side",
|
|
63
|
+
inset = false,
|
|
64
|
+
...props
|
|
65
|
+
}: React.ComponentProps<"div"> & { placement?: PanelPlacement; inset?: boolean }) {
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
data-slot="dialog-panel"
|
|
69
|
+
data-placement={placement}
|
|
70
|
+
data-inset={inset || undefined}
|
|
71
|
+
className={cn(
|
|
72
|
+
"relative flex flex-col justify-center bg-[var(--color-bg-canvas)]",
|
|
73
|
+
// inset panels are tighter — the 8px frame already adds breathing room
|
|
74
|
+
inset ? "p-4" : "p-6",
|
|
75
|
+
// side: a column down the left; top: a banner across the top
|
|
76
|
+
placement === "side" ? "shrink-0 sm:w-2/5" : "min-h-32",
|
|
77
|
+
// inset: float inside the dialog with an 8px frame on the dialog-facing
|
|
78
|
+
// edges only — the interior edge stays flush, the body padding separates
|
|
79
|
+
inset && "overflow-hidden [border-radius:var(--radius-card)]",
|
|
80
|
+
inset && (placement === "side" ? "ml-2 my-2" : "mt-2 mx-2"),
|
|
81
|
+
className
|
|
82
|
+
)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
50
88
|
function DialogContent({
|
|
51
89
|
className,
|
|
52
90
|
children,
|
|
@@ -57,31 +95,66 @@ function DialogContent({
|
|
|
57
95
|
showCloseButton?: boolean
|
|
58
96
|
container?: HTMLElement | null
|
|
59
97
|
}) {
|
|
98
|
+
// Detect an optional DialogPanel child and split it out from the body.
|
|
99
|
+
const childArray = React.Children.toArray(children)
|
|
100
|
+
const panel = childArray.find(
|
|
101
|
+
(c): c is React.ReactElement<{ placement?: PanelPlacement; children?: React.ReactNode }> =>
|
|
102
|
+
React.isValidElement(c) && (c.type as { displayName?: string })?.displayName === "DialogPanel"
|
|
103
|
+
)
|
|
104
|
+
const body = childArray.filter(c => c !== panel)
|
|
105
|
+
const placement = panel?.props.placement ?? "side"
|
|
106
|
+
const hasPanel = Boolean(panel)
|
|
107
|
+
|
|
108
|
+
const closeButton = showCloseButton && (
|
|
109
|
+
<DialogPrimitive.Close asChild>
|
|
110
|
+
<Button
|
|
111
|
+
data-slot="dialog-close"
|
|
112
|
+
variant="ghost"
|
|
113
|
+
size="icon-sm"
|
|
114
|
+
aria-label="Close"
|
|
115
|
+
className="absolute top-5 right-5 z-10 translate-y-[5px] text-current opacity-70 hover:opacity-100"
|
|
116
|
+
>
|
|
117
|
+
<XIcon />
|
|
118
|
+
</Button>
|
|
119
|
+
</DialogPrimitive.Close>
|
|
120
|
+
)
|
|
121
|
+
|
|
60
122
|
return (
|
|
61
123
|
<DialogPortal container={container}>
|
|
62
124
|
<DialogOverlay />
|
|
63
125
|
<DialogPrimitive.Content
|
|
64
126
|
data-slot="dialog-content"
|
|
127
|
+
data-panel={hasPanel ? placement : undefined}
|
|
65
128
|
className={cn(
|
|
66
|
-
"fixed top-[50%] left-[50%] [z-index:var(--z-modal)]
|
|
129
|
+
"fixed top-[50%] left-[50%] [z-index:var(--z-modal)] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] overflow-hidden [border-radius:var(--radius-modal)] border border-[var(--color-border-default)] bg-[var(--color-bg-overlay)] shadow-[var(--shadow-modal)] duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
|
|
130
|
+
// layout: plain (padded grid) vs panelled (flex split, panel bleeds to edges)
|
|
131
|
+
hasPanel
|
|
132
|
+
? placement === "side"
|
|
133
|
+
? "flex flex-col sm:flex-row sm:max-w-2xl"
|
|
134
|
+
: "flex flex-col"
|
|
135
|
+
: "grid gap-4 p-5",
|
|
67
136
|
className
|
|
68
137
|
)}
|
|
69
138
|
{...props}
|
|
70
139
|
>
|
|
71
|
-
{
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
140
|
+
{/* top: close lives inside the panel so it inherits the panel's foreground */}
|
|
141
|
+
{placement === "top" && panel
|
|
142
|
+
? React.cloneElement(panel, {}, panel.props.children, closeButton)
|
|
143
|
+
: panel}
|
|
144
|
+
{hasPanel ? (
|
|
145
|
+
<div data-slot="dialog-body" className="relative flex flex-1 flex-col gap-4 p-5">
|
|
146
|
+
{body}
|
|
147
|
+
</div>
|
|
148
|
+
) : (
|
|
149
|
+
body
|
|
80
150
|
)}
|
|
151
|
+
{/* side / no panel: close sits over the content body */}
|
|
152
|
+
{placement !== "top" && closeButton}
|
|
81
153
|
</DialogPrimitive.Content>
|
|
82
154
|
</DialogPortal>
|
|
83
155
|
)
|
|
84
156
|
}
|
|
157
|
+
DialogPanel.displayName = "DialogPanel"
|
|
85
158
|
|
|
86
159
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
87
160
|
return (
|
|
@@ -127,7 +200,7 @@ function DialogTitle({
|
|
|
127
200
|
return (
|
|
128
201
|
<DialogPrimitive.Title
|
|
129
202
|
data-slot="dialog-title"
|
|
130
|
-
className={cn("text-heading-
|
|
203
|
+
className={cn("text-heading-2-serif", className)}
|
|
131
204
|
{...props}
|
|
132
205
|
/>
|
|
133
206
|
)
|
|
@@ -154,6 +227,7 @@ export {
|
|
|
154
227
|
DialogFooter,
|
|
155
228
|
DialogHeader,
|
|
156
229
|
DialogOverlay,
|
|
230
|
+
DialogPanel,
|
|
157
231
|
DialogPortal,
|
|
158
232
|
DialogTitle,
|
|
159
233
|
DialogTrigger,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
export interface FlowNode {
|
|
6
|
+
/** Node label (party name). */
|
|
7
|
+
name: string
|
|
8
|
+
/** Current balance; null when hidden (e.g. balance not shown). */
|
|
9
|
+
current: number | null
|
|
10
|
+
/** Projected balance after the movement; null when no amount yet or hidden. */
|
|
11
|
+
after: number | null
|
|
12
|
+
/** Shown in place of a balance when `current` is null (e.g. "Master balance"). */
|
|
13
|
+
subtitle?: string
|
|
14
|
+
/** True when this node is an unfilled slot (e.g. nothing selected yet). */
|
|
15
|
+
empty?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type FormatFn = (n: number) => string
|
|
19
|
+
const defaultFormat: FormatFn = (n) => "$" + n.toLocaleString("en-US")
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Exact-dash rounded border via an SVG rect overlay (CSS border-dashed has a
|
|
23
|
+
* fixed dash pattern that can't be sized). 6px dash / 6px gap. Fills its
|
|
24
|
+
* relatively-positioned parent.
|
|
25
|
+
*/
|
|
26
|
+
function DashedBorder({ radius = 10 }: { radius?: number }) {
|
|
27
|
+
return (
|
|
28
|
+
<svg className="pointer-events-none absolute inset-0 size-full" aria-hidden>
|
|
29
|
+
<rect
|
|
30
|
+
x="0.75" y="0.75"
|
|
31
|
+
width="100%" height="100%"
|
|
32
|
+
rx={radius} ry={radius}
|
|
33
|
+
fill="none"
|
|
34
|
+
stroke="var(--color-border-default)"
|
|
35
|
+
strokeWidth="1.5"
|
|
36
|
+
strokeDasharray="6 6"
|
|
37
|
+
style={{ width: "calc(100% - 1.5px)", height: "calc(100% - 1.5px)" }}
|
|
38
|
+
/>
|
|
39
|
+
</svg>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface FlowDiagramProps {
|
|
44
|
+
/** Top node (fixed position). */
|
|
45
|
+
top: FlowNode
|
|
46
|
+
/** Bottom node (fixed position). */
|
|
47
|
+
bottom: FlowNode
|
|
48
|
+
/** The amount flowing; 0 = idle/empty state. */
|
|
49
|
+
amount: number
|
|
50
|
+
/** Flow direction — arrow + sheen travel this way. */
|
|
51
|
+
direction: "up" | "down"
|
|
52
|
+
/** Unit shown beside the amount. */
|
|
53
|
+
currency?: string
|
|
54
|
+
/** Custom amount/balance formatter (defaults to "$" + en-US grouping). */
|
|
55
|
+
format?: FormatFn
|
|
56
|
+
/** Optional content rendered below the diagram (e.g. a reference pill). */
|
|
57
|
+
footer?: React.ReactNode
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Vertical flow diagram for a value movement between two parties. Positions are
|
|
62
|
+
* FIXED (`top` / `bottom`); only the `direction` changes which way the arrow
|
|
63
|
+
* points and the light sheen travels. Each node shows its current balance,
|
|
64
|
+
* which demotes to a "was …" caption as the new (after) balance slides in once
|
|
65
|
+
* an amount is entered. Unfilled nodes render as a dashed empty slot. Neutral
|
|
66
|
+
* styling — suited to neutral transfers.
|
|
67
|
+
*/
|
|
68
|
+
function FlowDiagram({ top, bottom, amount, direction, currency = "COP", format = defaultFormat, footer }: FlowDiagramProps) {
|
|
69
|
+
const active = amount > 0
|
|
70
|
+
const glintKey = active ? amount : 0
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div data-slot="flow-diagram" className="flex flex-col items-center">
|
|
74
|
+
<style>{`
|
|
75
|
+
@keyframes flow-slide-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
|
76
|
+
@keyframes flow-pop { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
|
|
77
|
+
`}</style>
|
|
78
|
+
|
|
79
|
+
<FlowNodeCard key={top.empty ? "top-empty" : `top-${top.name}`} node={top} active={active} format={format} pop />
|
|
80
|
+
|
|
81
|
+
<Connector active={active} direction={direction} amount={amount} currency={currency} format={format} glintKey={glintKey} />
|
|
82
|
+
|
|
83
|
+
<FlowNodeCard key={bottom.empty ? "bottom-empty" : `bottom-${bottom.name}`} node={bottom} active={active} format={format} pop />
|
|
84
|
+
|
|
85
|
+
{footer && <div className="mt-4">{footer}</div>}
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Full connector height: enough for top gap + badge + bottom gap.
|
|
91
|
+
const CONNECTOR_H = 80
|
|
92
|
+
|
|
93
|
+
function Connector({
|
|
94
|
+
active, direction, amount, currency, format, glintKey,
|
|
95
|
+
}: {
|
|
96
|
+
active: boolean
|
|
97
|
+
direction: "up" | "down"
|
|
98
|
+
amount: number
|
|
99
|
+
currency: string
|
|
100
|
+
format: FormatFn
|
|
101
|
+
glintKey: number
|
|
102
|
+
}) {
|
|
103
|
+
const gid = React.useId()
|
|
104
|
+
const H = CONNECTOR_H
|
|
105
|
+
const headUp = direction === "up"
|
|
106
|
+
|
|
107
|
+
const head = headUp ? "M2 6 L7 0 L12 6" : `M2 ${H - 6} L7 ${H} L12 ${H - 6}`
|
|
108
|
+
const shapes = (
|
|
109
|
+
<>
|
|
110
|
+
<line x1="7" y1="0" x2="7" y2={H} />
|
|
111
|
+
<path d={head} />
|
|
112
|
+
</>
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div className="relative flex items-center justify-center my-1" style={{ height: H }}>
|
|
117
|
+
<svg width="14" height={H} viewBox={`0 0 14 ${H}`} className="block overflow-visible" aria-hidden>
|
|
118
|
+
<g stroke={active ? "var(--color-text-default)" : "var(--color-border-strong)"} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none">
|
|
119
|
+
{shapes}
|
|
120
|
+
</g>
|
|
121
|
+
{active && (
|
|
122
|
+
<>
|
|
123
|
+
<defs>
|
|
124
|
+
<linearGradient id={gid} gradientUnits="userSpaceOnUse" x1="7" y1="0" x2="7" y2="18">
|
|
125
|
+
<stop offset="0%" stopColor="var(--color-bg-raised)" stopOpacity="0" />
|
|
126
|
+
<stop offset="50%" stopColor="var(--color-bg-raised)" stopOpacity="0.5" />
|
|
127
|
+
<stop offset="100%" stopColor="var(--color-bg-raised)" stopOpacity="0" />
|
|
128
|
+
<animate attributeName="y1" values={direction === "down" ? `-20;${H}` : `${H};-20`} dur="2.4s" repeatCount="indefinite" />
|
|
129
|
+
<animate attributeName="y2" values={direction === "down" ? `-2;${H + 18}` : `${H + 18};-2`} dur="2.4s" repeatCount="indefinite" />
|
|
130
|
+
</linearGradient>
|
|
131
|
+
</defs>
|
|
132
|
+
<g stroke={`url(#${gid})`} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none">
|
|
133
|
+
{shapes}
|
|
134
|
+
</g>
|
|
135
|
+
</>
|
|
136
|
+
)}
|
|
137
|
+
</svg>
|
|
138
|
+
|
|
139
|
+
<span
|
|
140
|
+
key={glintKey}
|
|
141
|
+
className={`absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 px-2.5 py-1 [border-radius:var(--radius-badge)] text-label-mono transition-colors duration-300 ${
|
|
142
|
+
active
|
|
143
|
+
? "bg-[var(--color-bg-inverted)] text-[var(--color-text-inverse)]"
|
|
144
|
+
: "bg-[var(--color-bg-base)] text-[var(--color-text-hint)]"
|
|
145
|
+
}`}
|
|
146
|
+
>
|
|
147
|
+
{!active && <DashedBorder radius={16} />}
|
|
148
|
+
{active ? format(amount) : format(0)}
|
|
149
|
+
<span className={`text-caption ml-1 ${active ? "text-[var(--color-text-inverse-muted)]" : "text-[var(--color-text-muted)]"}`}>{currency}</span>
|
|
150
|
+
</span>
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function FlowNodeCard({ node, active, format, pop }: { node: FlowNode; active: boolean; format: FormatFn; pop?: boolean }) {
|
|
156
|
+
const showAfter = active && node.after !== null
|
|
157
|
+
const overdrawn = node.after !== null && node.after < 0
|
|
158
|
+
|
|
159
|
+
if (node.empty) {
|
|
160
|
+
return (
|
|
161
|
+
<div className="relative flex flex-col items-center justify-center gap-1 [border-radius:var(--radius-card)] bg-[var(--color-bg-base)] px-5 py-3 text-center min-w-[180px] min-h-[64px]">
|
|
162
|
+
<DashedBorder radius={10} />
|
|
163
|
+
<span className="text-body-sm text-[var(--color-text-hint)]">{node.name}</span>
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div
|
|
170
|
+
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]"
|
|
171
|
+
style={pop ? { animation: "flow-pop .18s cubic-bezier(0.16,1,0.3,1) both" } : undefined}
|
|
172
|
+
>
|
|
173
|
+
<span className="text-body-sm text-[var(--color-text-default)]">{node.name}</span>
|
|
174
|
+
|
|
175
|
+
{node.current === null ? (
|
|
176
|
+
node.subtitle ? <span className="text-caption text-[var(--color-text-muted)]">{node.subtitle}</span> : null
|
|
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)}
|
|
187
|
+
</span>
|
|
188
|
+
{showAfter && (
|
|
189
|
+
<span
|
|
190
|
+
className={`text-label-mono ${overdrawn ? "text-[var(--color-feedback-error)]" : "text-[var(--color-text-default)]"}`}
|
|
191
|
+
style={{ animation: "flow-slide-in .3s ease both" }}
|
|
192
|
+
>
|
|
193
|
+
{format(node.after!)}
|
|
194
|
+
</span>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export { FlowDiagram }
|
package/src/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ export * from "./components/ui/tab-count"
|
|
|
43
43
|
export * from "./components/ui/tabs"
|
|
44
44
|
export * from "./components/ui/textarea"
|
|
45
45
|
export * from "./components/ui/tooltip"
|
|
46
|
+
export * from "./components/ui/flow-diagram"
|
|
46
47
|
|
|
47
48
|
// Brand textures + logo
|
|
48
49
|
export * from "./textures"
|