@olympusoss/canvas 2.11.1 → 2.13.0
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,316 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
export type MetricBreakdownTone = "success" | "warning" | "error" | "neutral";
|
|
6
|
+
|
|
7
|
+
const TONE_HSL: Record<MetricBreakdownTone, string> = {
|
|
8
|
+
success: "143 70% 40%",
|
|
9
|
+
warning: "38 92% 50%",
|
|
10
|
+
error: "0 80% 60%",
|
|
11
|
+
neutral: "var(--muted-foreground)",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface MetricBreakdownRow {
|
|
15
|
+
/** Row label (often a code or category key). Used as the React key. */
|
|
16
|
+
label: React.ReactNode;
|
|
17
|
+
/** Numeric value rendered to the right of the label. */
|
|
18
|
+
value: number;
|
|
19
|
+
/**
|
|
20
|
+
* Period-over-period change as a percentage. Positive renders as `▲ N%` in
|
|
21
|
+
* the success hue, negative as `▼ N%` in the error hue. Omit to hide the
|
|
22
|
+
* delta column for that row.
|
|
23
|
+
*/
|
|
24
|
+
delta?: number;
|
|
25
|
+
/**
|
|
26
|
+
* CSS variable name (without leading `--`) used for the row's swatch and
|
|
27
|
+
* bar fill. Falls back to the wrapper's `defaultColorVar`.
|
|
28
|
+
*/
|
|
29
|
+
colorVar?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface MetricBreakdownChip {
|
|
33
|
+
/** Chip label, typically a code or short tag. Used as the React key. */
|
|
34
|
+
label: React.ReactNode;
|
|
35
|
+
/** Optional count rendered after a thin separator. */
|
|
36
|
+
count?: number;
|
|
37
|
+
/** Visual tone. Default `error`. */
|
|
38
|
+
tone?: MetricBreakdownTone;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface MetricBreakdownProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
42
|
+
/** Primary metric value rendered as the headline number. */
|
|
43
|
+
value: React.ReactNode;
|
|
44
|
+
/** Caption rendered below the headline value (uppercase muted). */
|
|
45
|
+
label: React.ReactNode;
|
|
46
|
+
/** Secondary metric rendered top-right (e.g. an error rate). */
|
|
47
|
+
rate?: React.ReactNode;
|
|
48
|
+
/** Caption for the secondary metric (uppercase muted). */
|
|
49
|
+
rateLabel?: React.ReactNode;
|
|
50
|
+
/** Tone driving the secondary metric color. Default `neutral`. */
|
|
51
|
+
rateTone?: MetricBreakdownTone;
|
|
52
|
+
/** Sparkline data points. Rendered as an SVG line + area-fill ramp. */
|
|
53
|
+
spark?: number[];
|
|
54
|
+
/** Unit suffix rendered next to the last value of the sparkline, e.g. `req/s`. */
|
|
55
|
+
sparkUnit?: React.ReactNode;
|
|
56
|
+
/**
|
|
57
|
+
* CSS variable name (without leading `--`) for the sparkline color. Default
|
|
58
|
+
* `chart-1`. Also used as the fallback for breakdown rows that omit `colorVar`.
|
|
59
|
+
*/
|
|
60
|
+
defaultColorVar?: string;
|
|
61
|
+
/** Pixel height of the sparkline. Default `36`. */
|
|
62
|
+
sparkHeight?: number;
|
|
63
|
+
/** Category breakdown rows. */
|
|
64
|
+
breakdown?: MetricBreakdownRow[];
|
|
65
|
+
/** Format breakdown row values. Default `toLocaleString`. */
|
|
66
|
+
valueFormatter?: (value: number) => string;
|
|
67
|
+
/** Trailing chip row (e.g. recent error codes). */
|
|
68
|
+
chips?: MetricBreakdownChip[];
|
|
69
|
+
/**
|
|
70
|
+
* Label rendered before the chip row (uppercase muted). Default `"Errors"`.
|
|
71
|
+
* Pass `null` to hide.
|
|
72
|
+
*/
|
|
73
|
+
chipsLabel?: React.ReactNode;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Multi-section metric card: a headline value with an optional rate, a small
|
|
78
|
+
* inline trend sparkline, a per-category breakdown with delta arrows, and a
|
|
79
|
+
* trailing chip row for recent issues.
|
|
80
|
+
*
|
|
81
|
+
* Designed for "throughput-style" dashboards (token issuance, API request
|
|
82
|
+
* volume, job throughput, sign-ups by source): anywhere a single metric needs
|
|
83
|
+
* to be decomposed by category and contextualized by trend and notable issues
|
|
84
|
+
* in one card. Identity-agnostic; bring your own labels and color tokens.
|
|
85
|
+
*/
|
|
86
|
+
export const MetricBreakdown = React.forwardRef<HTMLDivElement, MetricBreakdownProps>(
|
|
87
|
+
(
|
|
88
|
+
{
|
|
89
|
+
value,
|
|
90
|
+
label,
|
|
91
|
+
rate,
|
|
92
|
+
rateLabel,
|
|
93
|
+
rateTone = "neutral",
|
|
94
|
+
spark,
|
|
95
|
+
sparkUnit,
|
|
96
|
+
defaultColorVar = "chart-1",
|
|
97
|
+
sparkHeight = 36,
|
|
98
|
+
breakdown,
|
|
99
|
+
valueFormatter = (v) => v.toLocaleString(),
|
|
100
|
+
chips,
|
|
101
|
+
chipsLabel = "Errors",
|
|
102
|
+
className,
|
|
103
|
+
...props
|
|
104
|
+
},
|
|
105
|
+
ref,
|
|
106
|
+
) => {
|
|
107
|
+
const rateColor =
|
|
108
|
+
rateTone === "neutral" ? "hsl(var(--muted-foreground))" : `hsl(${TONE_HSL[rateTone]})`;
|
|
109
|
+
const showChipsLabel = chipsLabel !== null && chipsLabel !== undefined && chipsLabel !== "";
|
|
110
|
+
return (
|
|
111
|
+
<div ref={ref} className={cn("w-full", className)} {...props}>
|
|
112
|
+
<div className="mb-2.5 flex items-baseline justify-between gap-3">
|
|
113
|
+
<div>
|
|
114
|
+
<div className="font-mono text-[22px] font-semibold leading-[1.1] tabular-nums">
|
|
115
|
+
{value}
|
|
116
|
+
</div>
|
|
117
|
+
<div className="text-[11px] uppercase tracking-[0.04em] text-muted-foreground">
|
|
118
|
+
{label}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
{(rate !== undefined || rateLabel) && (
|
|
122
|
+
<div className="text-right">
|
|
123
|
+
<div
|
|
124
|
+
className="font-mono text-[13px] font-medium tabular-nums"
|
|
125
|
+
style={{ color: rateColor }}
|
|
126
|
+
>
|
|
127
|
+
{rate}
|
|
128
|
+
</div>
|
|
129
|
+
{rateLabel && (
|
|
130
|
+
<div className="text-[11px] uppercase tracking-[0.04em] text-muted-foreground">
|
|
131
|
+
{rateLabel}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{spark && spark.length > 1 && (
|
|
139
|
+
<MetricBreakdownSpark
|
|
140
|
+
data={spark}
|
|
141
|
+
height={sparkHeight}
|
|
142
|
+
colorVar={defaultColorVar}
|
|
143
|
+
unit={sparkUnit}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{breakdown && breakdown.length > 0 && (
|
|
148
|
+
<MetricBreakdownRows
|
|
149
|
+
rows={breakdown}
|
|
150
|
+
defaultColorVar={defaultColorVar}
|
|
151
|
+
valueFormatter={valueFormatter}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
{chips && chips.length > 0 && (
|
|
156
|
+
<div
|
|
157
|
+
className={cn(
|
|
158
|
+
"flex flex-wrap items-center gap-1.5",
|
|
159
|
+
(breakdown && breakdown.length > 0) || spark
|
|
160
|
+
? "mt-3 border-t border-border pt-2.5"
|
|
161
|
+
: "",
|
|
162
|
+
)}
|
|
163
|
+
>
|
|
164
|
+
{showChipsLabel && (
|
|
165
|
+
<span className="mr-0.5 text-[10.5px] uppercase tracking-[0.04em] text-muted-foreground">
|
|
166
|
+
{chipsLabel}
|
|
167
|
+
</span>
|
|
168
|
+
)}
|
|
169
|
+
{chips.map((chip, i) => {
|
|
170
|
+
const tone = chip.tone ?? "error";
|
|
171
|
+
const hsl = TONE_HSL[tone];
|
|
172
|
+
return (
|
|
173
|
+
<span
|
|
174
|
+
key={`${i}-${typeof chip.label === "string" ? chip.label : i}`}
|
|
175
|
+
className="inline-flex items-center gap-1 rounded font-mono text-[10.5px]"
|
|
176
|
+
style={{
|
|
177
|
+
padding: "2px 6px",
|
|
178
|
+
background: `hsl(${hsl} / 0.1)`,
|
|
179
|
+
color: `hsl(${hsl})`,
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
{chip.label}
|
|
183
|
+
{chip.count !== undefined && <span className="opacity-70">·{chip.count}</span>}
|
|
184
|
+
</span>
|
|
185
|
+
);
|
|
186
|
+
})}
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
MetricBreakdown.displayName = "MetricBreakdown";
|
|
194
|
+
|
|
195
|
+
interface MetricBreakdownSparkProps {
|
|
196
|
+
data: number[];
|
|
197
|
+
height: number;
|
|
198
|
+
colorVar: string;
|
|
199
|
+
unit?: React.ReactNode;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function MetricBreakdownSpark({ data, height, colorVar, unit }: MetricBreakdownSparkProps) {
|
|
203
|
+
const max = Math.max(...data);
|
|
204
|
+
const min = Math.min(...data);
|
|
205
|
+
const range = max - min || 1;
|
|
206
|
+
const w = (data.length - 1) * 10;
|
|
207
|
+
const h = height;
|
|
208
|
+
const pts = data
|
|
209
|
+
.map((v, i) => {
|
|
210
|
+
const x = i * 10;
|
|
211
|
+
const y = h - 2 - ((v - min) / range) * (h - 6);
|
|
212
|
+
return `${x},${y}`;
|
|
213
|
+
})
|
|
214
|
+
.join(" ");
|
|
215
|
+
const area = `0,${h} ${pts} ${w},${h}`;
|
|
216
|
+
const last = data[data.length - 1];
|
|
217
|
+
const lastY = h - 2 - ((last - min) / range) * (h - 6);
|
|
218
|
+
const fillId = React.useId();
|
|
219
|
+
return (
|
|
220
|
+
<div className="relative mb-3.5" style={{ height }}>
|
|
221
|
+
<svg
|
|
222
|
+
viewBox={`0 0 ${w} ${h}`}
|
|
223
|
+
preserveAspectRatio="none"
|
|
224
|
+
className="h-full w-full overflow-visible"
|
|
225
|
+
aria-hidden
|
|
226
|
+
>
|
|
227
|
+
<defs>
|
|
228
|
+
<linearGradient id={fillId} x1="0" x2="0" y1="0" y2="1">
|
|
229
|
+
<stop offset="0%" stopColor={`hsl(var(--${colorVar}))`} stopOpacity="0.25" />
|
|
230
|
+
<stop offset="100%" stopColor={`hsl(var(--${colorVar}))`} stopOpacity="0" />
|
|
231
|
+
</linearGradient>
|
|
232
|
+
</defs>
|
|
233
|
+
<polygon points={area} fill={`url(#${fillId})`} />
|
|
234
|
+
<polyline
|
|
235
|
+
points={pts}
|
|
236
|
+
fill="none"
|
|
237
|
+
stroke={`hsl(var(--${colorVar}))`}
|
|
238
|
+
strokeWidth="1.5"
|
|
239
|
+
vectorEffect="non-scaling-stroke"
|
|
240
|
+
/>
|
|
241
|
+
<circle cx={w} cy={lastY} r="2.5" fill={`hsl(var(--${colorVar}))`} />
|
|
242
|
+
</svg>
|
|
243
|
+
{unit !== undefined && (
|
|
244
|
+
<div
|
|
245
|
+
className="absolute right-0 top-0 bg-card px-1 font-mono text-[11px]"
|
|
246
|
+
style={{ color: `hsl(var(--${colorVar}))` }}
|
|
247
|
+
>
|
|
248
|
+
{last}
|
|
249
|
+
{typeof unit === "string" ? ` ${unit}` : <> {unit}</>}
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
interface MetricBreakdownRowsProps {
|
|
257
|
+
rows: MetricBreakdownRow[];
|
|
258
|
+
defaultColorVar: string;
|
|
259
|
+
valueFormatter: (value: number) => string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function MetricBreakdownRows({ rows, defaultColorVar, valueFormatter }: MetricBreakdownRowsProps) {
|
|
263
|
+
const total = rows.reduce((sum, r) => sum + r.value, 0) || 1;
|
|
264
|
+
return (
|
|
265
|
+
<div className="flex flex-col gap-2">
|
|
266
|
+
{rows.map((row, i) => {
|
|
267
|
+
const colorVar = row.colorVar ?? defaultColorVar;
|
|
268
|
+
const pct = (row.value / total) * 100;
|
|
269
|
+
const hasDelta = row.delta !== undefined;
|
|
270
|
+
const up = hasDelta && (row.delta as number) >= 0;
|
|
271
|
+
const deltaHsl = up ? TONE_HSL.success : TONE_HSL.error;
|
|
272
|
+
return (
|
|
273
|
+
<div
|
|
274
|
+
key={`${i}-${typeof row.label === "string" ? row.label : i}`}
|
|
275
|
+
className="text-[12.5px]"
|
|
276
|
+
>
|
|
277
|
+
<div className="mb-1 flex items-center gap-2">
|
|
278
|
+
<span
|
|
279
|
+
className="size-2 shrink-0 rounded-sm"
|
|
280
|
+
style={{ background: `hsl(var(--${colorVar}))` }}
|
|
281
|
+
aria-hidden
|
|
282
|
+
/>
|
|
283
|
+
<span className="flex-1 truncate font-mono text-[11.5px] text-foreground">
|
|
284
|
+
{row.label}
|
|
285
|
+
</span>
|
|
286
|
+
<span className="font-mono text-[11.5px] tabular-nums">
|
|
287
|
+
{valueFormatter(row.value)}
|
|
288
|
+
</span>
|
|
289
|
+
{hasDelta && (
|
|
290
|
+
<span
|
|
291
|
+
className="min-w-[38px] text-right font-mono text-[10.5px] tabular-nums"
|
|
292
|
+
style={{ color: `hsl(${deltaHsl})` }}
|
|
293
|
+
>
|
|
294
|
+
{up ? "▲" : "▼"} {Math.abs(row.delta as number)}%
|
|
295
|
+
</span>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
<div
|
|
299
|
+
className="overflow-hidden rounded-full bg-muted"
|
|
300
|
+
style={{ height: 3 }}
|
|
301
|
+
aria-hidden
|
|
302
|
+
>
|
|
303
|
+
<div
|
|
304
|
+
className="h-full rounded-full"
|
|
305
|
+
style={{
|
|
306
|
+
width: `${pct}%`,
|
|
307
|
+
background: `hsl(var(--${colorVar}))`,
|
|
308
|
+
}}
|
|
309
|
+
/>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
})}
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
export interface OrSeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
/**
|
|
7
|
+
* Text rendered between the two rule segments.
|
|
8
|
+
* @default "or"
|
|
9
|
+
*/
|
|
10
|
+
label?: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Horizontal divider with a centred label, e.g. between social-provider
|
|
15
|
+
* buttons and an email/password form. Two `border-t` rules flank a short
|
|
16
|
+
* upper-case label sitting on the card background.
|
|
17
|
+
*
|
|
18
|
+
* Place inside a card whose background is `bg-card`; the label inherits
|
|
19
|
+
* that surface so the rules visually meet behind it.
|
|
20
|
+
*/
|
|
21
|
+
const OrSeparator = React.forwardRef<HTMLDivElement, OrSeparatorProps>(
|
|
22
|
+
({ label = "or", className, ...props }, ref) => (
|
|
23
|
+
<div
|
|
24
|
+
ref={ref}
|
|
25
|
+
role="separator"
|
|
26
|
+
aria-orientation="horizontal"
|
|
27
|
+
className={cn("flex items-center gap-3 py-1", className)}
|
|
28
|
+
{...props}
|
|
29
|
+
>
|
|
30
|
+
<div className="h-px flex-1 bg-border" />
|
|
31
|
+
<span className="text-[11px] uppercase tracking-[0.08em] text-muted-foreground">{label}</span>
|
|
32
|
+
<div className="h-px flex-1 bg-border" />
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
OrSeparator.displayName = "OrSeparator";
|
|
37
|
+
|
|
38
|
+
export { OrSeparator };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import { Button } from "../atoms/button";
|
|
7
|
+
|
|
8
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
9
|
+
Brand glyphs. Tiny, currentColor for monochrome marks
|
|
10
|
+
(GitHub, Microsoft outline, generic SSO). Multi-color marks
|
|
11
|
+
(Google, Apple) inline their official palette.
|
|
12
|
+
────────────────────────────────────────────────────────────────── */
|
|
13
|
+
|
|
14
|
+
const GitHubGlyph = () => (
|
|
15
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
|
16
|
+
<title>GitHub</title>
|
|
17
|
+
<path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.1.79-.25.79-.56v-2c-3.2.69-3.87-1.54-3.87-1.54-.52-1.33-1.27-1.69-1.27-1.69-1.04-.71.08-.7.08-.7 1.15.08 1.75 1.18 1.75 1.18 1.02 1.75 2.68 1.24 3.34.95.1-.74.4-1.24.72-1.52-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.28 1.18-3.08-.12-.29-.51-1.46.11-3.04 0 0 .97-.31 3.18 1.18a11 11 0 0 1 5.78 0c2.21-1.49 3.18-1.18 3.18-1.18.62 1.58.23 2.75.11 3.04.74.8 1.18 1.82 1.18 3.08 0 4.42-2.69 5.39-5.26 5.68.41.35.77 1.04.77 2.11v3.13c0 .31.21.67.79.56A11.51 11.51 0 0 0 23.5 12c0-6.35-5.15-11.5-11.5-11.5z" />
|
|
18
|
+
</svg>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const GoogleGlyph = () => (
|
|
22
|
+
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden>
|
|
23
|
+
<title>Google</title>
|
|
24
|
+
<path
|
|
25
|
+
fill="#4285F4"
|
|
26
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.76h3.56c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
27
|
+
/>
|
|
28
|
+
<path
|
|
29
|
+
fill="#34A853"
|
|
30
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.56-2.76c-.98.66-2.24 1.06-3.72 1.06-2.86 0-5.29-1.93-6.15-4.53H2.18v2.84A11 11 0 0 0 12 23z"
|
|
31
|
+
/>
|
|
32
|
+
<path
|
|
33
|
+
fill="#FBBC05"
|
|
34
|
+
d="M5.85 14.11A6.6 6.6 0 0 1 5.5 12c0-.73.13-1.44.35-2.11V7.05H2.18A11 11 0 0 0 1 12c0 1.78.43 3.46 1.18 4.95l3.67-2.84z"
|
|
35
|
+
/>
|
|
36
|
+
<path
|
|
37
|
+
fill="#EA4335"
|
|
38
|
+
d="M12 5.38c1.62 0 3.06.56 4.2 1.64l3.15-3.15C17.45 2.1 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.05l3.67 2.84C6.71 7.31 9.14 5.38 12 5.38z"
|
|
39
|
+
/>
|
|
40
|
+
</svg>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const AppleGlyph = () => (
|
|
44
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
|
45
|
+
<title>Apple</title>
|
|
46
|
+
<path d="M17.05 12.04c-.03-2.85 2.32-4.22 2.43-4.29-1.32-1.94-3.39-2.21-4.12-2.24-1.75-.18-3.42 1.03-4.31 1.03-.9 0-2.27-1.01-3.74-.98-1.92.03-3.7 1.12-4.69 2.83-2.01 3.47-.51 8.6 1.43 11.43.96 1.38 2.09 2.93 3.56 2.87 1.44-.06 1.98-.92 3.71-.92 1.73 0 2.22.92 3.74.89 1.54-.03 2.52-1.4 3.46-2.79 1.1-1.6 1.55-3.15 1.58-3.23-.03-.01-3.03-1.16-3.05-4.6zM14.27 3.65c.79-.96 1.32-2.29 1.18-3.62-1.14.05-2.52.76-3.34 1.71-.73.84-1.37 2.19-1.2 3.49 1.27.1 2.57-.65 3.36-1.58z" />
|
|
47
|
+
</svg>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const MicrosoftGlyph = () => (
|
|
51
|
+
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden>
|
|
52
|
+
<title>Microsoft</title>
|
|
53
|
+
<path fill="#F25022" d="M1 1h10v10H1z" />
|
|
54
|
+
<path fill="#7FBA00" d="M13 1h10v10H13z" />
|
|
55
|
+
<path fill="#00A4EF" d="M1 13h10v10H1z" />
|
|
56
|
+
<path fill="#FFB900" d="M13 13h10v10H13z" />
|
|
57
|
+
</svg>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const SsoGlyph = () => (
|
|
61
|
+
<svg
|
|
62
|
+
width="16"
|
|
63
|
+
height="16"
|
|
64
|
+
viewBox="0 0 24 24"
|
|
65
|
+
fill="none"
|
|
66
|
+
stroke="currentColor"
|
|
67
|
+
strokeWidth={2}
|
|
68
|
+
strokeLinecap="round"
|
|
69
|
+
strokeLinejoin="round"
|
|
70
|
+
aria-hidden
|
|
71
|
+
>
|
|
72
|
+
<title>SSO</title>
|
|
73
|
+
<rect x="3" y="11" width="18" height="11" rx="2" />
|
|
74
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
75
|
+
</svg>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Known social/SSO providers. Each entry pairs a glyph with a default
|
|
80
|
+
* label; the caller can override the label per-button if needed.
|
|
81
|
+
*/
|
|
82
|
+
export type SocialProvider = "github" | "google" | "apple" | "microsoft" | "sso";
|
|
83
|
+
|
|
84
|
+
interface ProviderMeta {
|
|
85
|
+
glyph: React.ReactNode;
|
|
86
|
+
label: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const PROVIDERS: Record<SocialProvider, ProviderMeta> = {
|
|
90
|
+
github: { glyph: <GitHubGlyph />, label: "Continue with GitHub" },
|
|
91
|
+
google: { glyph: <GoogleGlyph />, label: "Continue with Google" },
|
|
92
|
+
apple: { glyph: <AppleGlyph />, label: "Continue with Apple" },
|
|
93
|
+
microsoft: { glyph: <MicrosoftGlyph />, label: "Continue with Microsoft" },
|
|
94
|
+
sso: { glyph: <SsoGlyph />, label: "Continue with SSO" },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export interface SocialButtonProps
|
|
98
|
+
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
|
|
99
|
+
/** Provider identifier. Picks the glyph and default label. */
|
|
100
|
+
provider: SocialProvider;
|
|
101
|
+
/** Override the default label (e.g. "Continue with Okta"). */
|
|
102
|
+
label?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Single provider button. Outline variant with the provider glyph on the
|
|
107
|
+
* leading edge. Use directly when you want one provider, or compose
|
|
108
|
+
* several inside `SocialButtons`.
|
|
109
|
+
*/
|
|
110
|
+
const SocialButton = React.forwardRef<HTMLButtonElement, SocialButtonProps>(
|
|
111
|
+
({ provider, label, className, ...props }, ref) => {
|
|
112
|
+
const meta = PROVIDERS[provider];
|
|
113
|
+
return (
|
|
114
|
+
<Button
|
|
115
|
+
ref={ref}
|
|
116
|
+
type="button"
|
|
117
|
+
variant="outline"
|
|
118
|
+
className={cn("w-full justify-center gap-2", className)}
|
|
119
|
+
{...props}
|
|
120
|
+
>
|
|
121
|
+
{meta.glyph}
|
|
122
|
+
<span>{label ?? meta.label}</span>
|
|
123
|
+
</Button>
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
SocialButton.displayName = "SocialButton";
|
|
128
|
+
|
|
129
|
+
export interface SocialButtonsProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
130
|
+
/**
|
|
131
|
+
* Provider list rendered vertically. Pass `["github", "google"]` for the
|
|
132
|
+
* common CIAM case, or `["sso"]` for a single corporate SSO button.
|
|
133
|
+
* Defaults to `["github", "google"]`.
|
|
134
|
+
*/
|
|
135
|
+
providers?: SocialProvider[];
|
|
136
|
+
/** Per-provider click handler. Receives the provider id. */
|
|
137
|
+
onProviderClick?: (provider: SocialProvider) => void;
|
|
138
|
+
/** When true, all buttons render disabled (e.g. while signing in). */
|
|
139
|
+
disabled?: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Vertical stack of social/SSO provider buttons. Typically placed at the
|
|
144
|
+
* top of a sign-in or sign-up card, above an `OrSeparator`.
|
|
145
|
+
*
|
|
146
|
+
* Stays purely presentational: the consumer wires `onProviderClick` to
|
|
147
|
+
* the OAuth2 initiation flow.
|
|
148
|
+
*/
|
|
149
|
+
const SocialButtons = React.forwardRef<HTMLDivElement, SocialButtonsProps>(
|
|
150
|
+
({ providers = ["github", "google"], onProviderClick, disabled, className, ...props }, ref) => (
|
|
151
|
+
<div ref={ref} className={cn("flex flex-col gap-2.5", className)} {...props}>
|
|
152
|
+
{providers.map((p) => (
|
|
153
|
+
<SocialButton
|
|
154
|
+
key={p}
|
|
155
|
+
provider={p}
|
|
156
|
+
disabled={disabled}
|
|
157
|
+
onClick={() => onProviderClick?.(p)}
|
|
158
|
+
/>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
SocialButtons.displayName = "SocialButtons";
|
|
164
|
+
|
|
165
|
+
export { SocialButton, SocialButtons };
|
package/src/index.ts
CHANGED
|
@@ -101,6 +101,13 @@ export {
|
|
|
101
101
|
type LabeledBarListItem,
|
|
102
102
|
type LabeledBarListProps,
|
|
103
103
|
} from "./components/charts/labeled-bar-list";
|
|
104
|
+
export {
|
|
105
|
+
MetricBreakdown,
|
|
106
|
+
type MetricBreakdownChip,
|
|
107
|
+
type MetricBreakdownProps,
|
|
108
|
+
type MetricBreakdownRow,
|
|
109
|
+
type MetricBreakdownTone,
|
|
110
|
+
} from "./components/charts/metric-breakdown";
|
|
104
111
|
export {
|
|
105
112
|
ReferenceArea,
|
|
106
113
|
ReferenceDot,
|
|
@@ -223,6 +230,10 @@ export {
|
|
|
223
230
|
NumberBadge,
|
|
224
231
|
type NumberBadgeProps,
|
|
225
232
|
} from "./components/molecules/number-badge";
|
|
233
|
+
export {
|
|
234
|
+
OrSeparator,
|
|
235
|
+
type OrSeparatorProps,
|
|
236
|
+
} from "./components/molecules/or-separator";
|
|
226
237
|
export {
|
|
227
238
|
PageHeader,
|
|
228
239
|
type PageHeaderBreadcrumb,
|
|
@@ -267,6 +278,13 @@ export {
|
|
|
267
278
|
SectionCard,
|
|
268
279
|
type SectionCardProps,
|
|
269
280
|
} from "./components/molecules/section-card";
|
|
281
|
+
export {
|
|
282
|
+
SocialButton,
|
|
283
|
+
type SocialButtonProps,
|
|
284
|
+
SocialButtons,
|
|
285
|
+
type SocialButtonsProps,
|
|
286
|
+
type SocialProvider,
|
|
287
|
+
} from "./components/molecules/social-buttons";
|
|
270
288
|
export { StatCard, type StatCardProps } from "./components/molecules/stat-card";
|
|
271
289
|
export {
|
|
272
290
|
StatusBadge,
|