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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minka-ds",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Minka product design system — tokenized component library",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -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 && <div className="mt-4">{footer}</div>}
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 ? "var(--color-text-default)" : "var(--color-border-strong)"} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none">
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"