@lovett/ui 0.0.4 → 0.0.5
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/dist/index.d.ts +2495 -0
- package/dist/index.js +14038 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +562 -0
- package/dist/tokens.css +579 -0
- package/package.json +26 -16
- package/src/allocation-sparkbar.tsx +90 -0
- package/src/floating-status-bar.tsx +112 -0
- package/src/index.ts +11 -0
- package/src/microsoft-logo.tsx +33 -0
- package/src/token-badge.tsx +92 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AllocationSparkbar — a compact per-period bar strip.
|
|
3
|
+
*
|
|
4
|
+
* New in ADR-123 (Budget Flighting redesign). Renders one bar per period
|
|
5
|
+
* (month), height proportional to the value; a zero/paused period renders
|
|
6
|
+
* short + dashed in a muted tone. Each bar can tint from its own token (e.g. a
|
|
7
|
+
* flight-group color), falling back to `baseColorVar`. Optional single-letter
|
|
8
|
+
* labels sit under each bar.
|
|
9
|
+
*
|
|
10
|
+
* Token discipline: colors come only from public token *names* via
|
|
11
|
+
* `rgb(var(--token) / a)`. No literals. Layout sizes use the spacing scale.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface AllocationSparkbarProps {
|
|
15
|
+
/** Per-period amounts. A value of 0 is treated as paused (short + dashed). */
|
|
16
|
+
values: number[]
|
|
17
|
+
/** Base token name for normal bars, e.g. `--folder-teal`. Includes `--`. */
|
|
18
|
+
baseColorVar: string
|
|
19
|
+
/** Optional per-period token name (e.g. a flight-group color). `null`/
|
|
20
|
+
* `undefined` falls back to `baseColorVar`. Length should match `values`. */
|
|
21
|
+
colorVars?: (string | null | undefined)[]
|
|
22
|
+
/** Optional short labels under each bar (e.g. `['J','F','M']`). */
|
|
23
|
+
labels?: string[]
|
|
24
|
+
/** Optional per-bar tooltip text (native `title`). */
|
|
25
|
+
titles?: string[]
|
|
26
|
+
/** Strip height in px. Default 36. */
|
|
27
|
+
height?: number
|
|
28
|
+
className?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function AllocationSparkbar({
|
|
32
|
+
values,
|
|
33
|
+
baseColorVar,
|
|
34
|
+
colorVars,
|
|
35
|
+
labels,
|
|
36
|
+
titles,
|
|
37
|
+
height = 36,
|
|
38
|
+
className,
|
|
39
|
+
}: AllocationSparkbarProps) {
|
|
40
|
+
const max = Math.max(...values, 1)
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
className={className}
|
|
44
|
+
style={{ display: 'flex', gap: 'var(--space-1)', alignItems: 'flex-end', height }}
|
|
45
|
+
>
|
|
46
|
+
{values.map((value, i) => {
|
|
47
|
+
const paused = value === 0
|
|
48
|
+
const barH = paused ? 4 : 8 + (value / max) * (height - 12)
|
|
49
|
+
const colorVar = colorVars?.[i] ?? baseColorVar
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
key={i}
|
|
53
|
+
title={titles?.[i]}
|
|
54
|
+
style={{
|
|
55
|
+
flex: 1,
|
|
56
|
+
display: 'flex',
|
|
57
|
+
flexDirection: 'column',
|
|
58
|
+
alignItems: 'center',
|
|
59
|
+
gap: 'var(--space-1)',
|
|
60
|
+
minWidth: 0,
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<div
|
|
64
|
+
style={{
|
|
65
|
+
width: '100%',
|
|
66
|
+
height: barH,
|
|
67
|
+
borderRadius: 'var(--radius-xs)',
|
|
68
|
+
background: paused
|
|
69
|
+
? 'rgb(var(--text-muted) / 0.18)'
|
|
70
|
+
: `rgb(var(${colorVar}) / 0.67)`,
|
|
71
|
+
border: paused
|
|
72
|
+
? '1px dashed rgb(var(--text-muted) / 0.4)'
|
|
73
|
+
: 'none',
|
|
74
|
+
transition: 'background var(--dur-fast, 120ms)',
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
{labels?.[i] !== undefined && (
|
|
78
|
+
<span
|
|
79
|
+
className="text-[9.5px] font-semibold tracking-[0.02em]"
|
|
80
|
+
style={{ color: 'rgb(var(--text-muted))' }}
|
|
81
|
+
>
|
|
82
|
+
{labels[i]}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
})}
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FloatingStatusBar — a bottom-center floating status + summary + action bar.
|
|
3
|
+
*
|
|
4
|
+
* Promoted in ADR-123 (Budget Flighting redesign). The lens's budget tracker
|
|
5
|
+
* and the creative-checker recon's "StatusFooter" both want this shape, so it
|
|
6
|
+
* graduates to @lovett/ui. A tone dot + label, an optional secondary meta, an
|
|
7
|
+
* optional summary node after a divider, and an optional single action slot.
|
|
8
|
+
*
|
|
9
|
+
* Honors `prefers-reduced-motion`: the entrance fade/slide is applied only
|
|
10
|
+
* under `motion-safe`. Token discipline: all colors via public tokens.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ReactNode } from 'react'
|
|
14
|
+
|
|
15
|
+
import { cn } from './lib/utils'
|
|
16
|
+
|
|
17
|
+
export type FloatingStatusTone =
|
|
18
|
+
| 'neutral'
|
|
19
|
+
| 'accent'
|
|
20
|
+
| 'success'
|
|
21
|
+
| 'warning'
|
|
22
|
+
| 'destructive'
|
|
23
|
+
| 'info'
|
|
24
|
+
|
|
25
|
+
const TONE_VAR: Record<FloatingStatusTone, string> = {
|
|
26
|
+
neutral: '--text-secondary',
|
|
27
|
+
accent: '--accent',
|
|
28
|
+
success: '--success',
|
|
29
|
+
warning: '--warning',
|
|
30
|
+
destructive: '--destructive',
|
|
31
|
+
info: '--info',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FloatingStatusBarProps {
|
|
35
|
+
/** Tone for the status dot + label color. */
|
|
36
|
+
tone: FloatingStatusTone
|
|
37
|
+
/** Primary status label (tone-colored, bold). */
|
|
38
|
+
label: ReactNode
|
|
39
|
+
/** Optional muted text after the label (e.g. a percentage). */
|
|
40
|
+
meta?: ReactNode
|
|
41
|
+
/** Optional summary node shown after a vertical divider (e.g. `$X → $Y`). */
|
|
42
|
+
summary?: ReactNode
|
|
43
|
+
/** Optional single action element (typically a secondary Button). */
|
|
44
|
+
action?: ReactNode
|
|
45
|
+
/** Override the fixed positioning (e.g. to embed in a story). Default fixed
|
|
46
|
+
* bottom-center. */
|
|
47
|
+
className?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function FloatingStatusBar({
|
|
51
|
+
tone,
|
|
52
|
+
label,
|
|
53
|
+
meta,
|
|
54
|
+
summary,
|
|
55
|
+
action,
|
|
56
|
+
className,
|
|
57
|
+
}: FloatingStatusBarProps) {
|
|
58
|
+
const toneColor = `rgb(var(${TONE_VAR[tone]}))`
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
role="status"
|
|
62
|
+
className={cn(
|
|
63
|
+
'fixed bottom-4 left-1/2 z-30 flex -translate-x-1/2 items-center gap-3.5 px-3.5 py-2.5',
|
|
64
|
+
'motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-2',
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
style={{
|
|
68
|
+
background: 'rgb(var(--bg-card))',
|
|
69
|
+
border: '1px solid rgb(var(--border-card))',
|
|
70
|
+
borderRadius: 'var(--radius-xl)',
|
|
71
|
+
boxShadow: 'var(--shadow-lg)',
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<div className="flex items-center gap-2">
|
|
75
|
+
<span
|
|
76
|
+
aria-hidden
|
|
77
|
+
className="size-2 shrink-0 rounded-full"
|
|
78
|
+
style={{ background: toneColor }}
|
|
79
|
+
/>
|
|
80
|
+
<span className="text-[12.5px] font-bold" style={{ color: toneColor }}>
|
|
81
|
+
{label}
|
|
82
|
+
</span>
|
|
83
|
+
{meta !== undefined && (
|
|
84
|
+
<span
|
|
85
|
+
className="text-[11px] tabular-nums"
|
|
86
|
+
style={{ color: 'rgb(var(--text-tertiary))' }}
|
|
87
|
+
>
|
|
88
|
+
{meta}
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{summary !== undefined && (
|
|
94
|
+
<>
|
|
95
|
+
<span
|
|
96
|
+
aria-hidden
|
|
97
|
+
className="h-[18px] w-px"
|
|
98
|
+
style={{ background: 'rgb(var(--border))' }}
|
|
99
|
+
/>
|
|
100
|
+
<span
|
|
101
|
+
className="text-[11.5px]"
|
|
102
|
+
style={{ color: 'rgb(var(--text-tertiary))' }}
|
|
103
|
+
>
|
|
104
|
+
{summary}
|
|
105
|
+
</span>
|
|
106
|
+
</>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{action}
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export { default as BrandLogoTile, type BrandLogoTileProps, type BrandLogoTileSi
|
|
|
23
23
|
export { default as ProfileSection, type ProfileSectionProps } from './profile-section'
|
|
24
24
|
export { default as ChipNav, type ChipNavItem } from './chip-nav'
|
|
25
25
|
export { default as CompletionRing, type CompletionRingProps } from './completion-ring'
|
|
26
|
+
export { default as MicrosoftLogo, type MicrosoftLogoProps } from './microsoft-logo'
|
|
26
27
|
export { default as Choropleth, type ChoroplethProps } from './choropleth'
|
|
27
28
|
export {
|
|
28
29
|
default as PageShell,
|
|
@@ -76,6 +77,16 @@ export { MetricCard, type MetricCardProps, type MetricCardTone } from './metric-
|
|
|
76
77
|
export { ProgressBar, type ProgressBarProps } from './progress-bar'
|
|
77
78
|
export { StepLoader, type StepLoaderProps, type StepLoaderStep } from './step-loader'
|
|
78
79
|
export { Badge, type BadgeProps, type BadgeTone } from './badge'
|
|
80
|
+
export { TokenBadge, type TokenBadgeProps } from './token-badge'
|
|
81
|
+
export {
|
|
82
|
+
AllocationSparkbar,
|
|
83
|
+
type AllocationSparkbarProps,
|
|
84
|
+
} from './allocation-sparkbar'
|
|
85
|
+
export {
|
|
86
|
+
FloatingStatusBar,
|
|
87
|
+
type FloatingStatusBarProps,
|
|
88
|
+
type FloatingStatusTone,
|
|
89
|
+
} from './floating-status-bar'
|
|
79
90
|
export { StatsGrid, type StatsGridProps } from './stats-grid'
|
|
80
91
|
export {
|
|
81
92
|
SortableTable,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MicrosoftLogo — the Microsoft four-square brand mark, for "Sign in with
|
|
3
|
+
* Microsoft" SSO buttons (ADR-124 D8). Lucide has no Microsoft glyph, so this
|
|
4
|
+
* is a bespoke SVG primitive — which `@lovett/ui` is the one allowed home for
|
|
5
|
+
* (CLAUDE.md §2/§6). The four square colours are Microsoft's required brand
|
|
6
|
+
* colours (per their identity guidelines); they are intrinsic to the mark, not
|
|
7
|
+
* theme tokens, exactly like BrandLogoTile's imagery — so they live here as
|
|
8
|
+
* literals rather than `tokens.css` variables.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface MicrosoftLogoProps {
|
|
12
|
+
/** Square size in px. Default 18 (matches a control-height button glyph). */
|
|
13
|
+
size?: number
|
|
14
|
+
className?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function MicrosoftLogo({ size = 18, className }: MicrosoftLogoProps) {
|
|
18
|
+
return (
|
|
19
|
+
<svg
|
|
20
|
+
width={size}
|
|
21
|
+
height={size}
|
|
22
|
+
viewBox="0 0 21 21"
|
|
23
|
+
className={className}
|
|
24
|
+
aria-hidden="true"
|
|
25
|
+
focusable="false"
|
|
26
|
+
>
|
|
27
|
+
<rect x="1" y="1" width="9" height="9" fill="#f25022" />
|
|
28
|
+
<rect x="11" y="1" width="9" height="9" fill="#7fba00" />
|
|
29
|
+
<rect x="1" y="11" width="9" height="9" fill="#00a4ef" />
|
|
30
|
+
<rect x="11" y="11" width="9" height="9" fill="#ffb900" />
|
|
31
|
+
</svg>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TokenBadge — tinted, token-driven category badge.
|
|
3
|
+
*
|
|
4
|
+
* Promoted in ADR-123 (Budget Flighting redesign) from the lens-local
|
|
5
|
+
* `TacticBadge`. Distinct from `<Badge>`: Badge carries only the 6 fixed
|
|
6
|
+
* semantic tones (success / warning / …); TokenBadge tints from ANY public
|
|
7
|
+
* token base name, so a categorical palette like `--folder-*` (tactic colors,
|
|
8
|
+
* flight-group colors) works. The lens keeps a thin `TacticBadge` wrapper that
|
|
9
|
+
* passes the matching `--folder-*` token.
|
|
10
|
+
*
|
|
11
|
+
* Token discipline: the only color input is a public token *name*; fill/border/
|
|
12
|
+
* text are derived from it via `rgb(var(--token) / a)`. No literals.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ReactNode } from 'react'
|
|
16
|
+
|
|
17
|
+
import { cn } from './lib/utils'
|
|
18
|
+
|
|
19
|
+
export interface TokenBadgeProps {
|
|
20
|
+
/** Public token name to tint from, e.g. `--folder-teal`, `--accent`,
|
|
21
|
+
* `--success`. Must include the leading `--`. */
|
|
22
|
+
colorVar: string
|
|
23
|
+
children: ReactNode
|
|
24
|
+
/** `soft` = tinted fill (default). `outline` = transparent fill + tinted
|
|
25
|
+
* border. */
|
|
26
|
+
variant?: 'soft' | 'outline'
|
|
27
|
+
/** Optional leading status dot in the token color. Ignored when `icon` set. */
|
|
28
|
+
dot?: boolean
|
|
29
|
+
/** Optional leading icon (12–14px expected). */
|
|
30
|
+
icon?: ReactNode
|
|
31
|
+
/** `sm` (10px) · `md` (11px, default) · `lg` (12px, taller). */
|
|
32
|
+
size?: 'sm' | 'md' | 'lg'
|
|
33
|
+
/** Pill (`full`) vs squared category tag (`tag`, default). */
|
|
34
|
+
shape?: 'tag' | 'pill'
|
|
35
|
+
/** Uppercase + wider tracking for short category codes (PRG, YT…). */
|
|
36
|
+
uppercase?: boolean
|
|
37
|
+
className?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const SIZE_CLASS: Record<NonNullable<TokenBadgeProps['size']>, string> = {
|
|
41
|
+
sm: 'h-[18px] px-1.5 text-[10px]',
|
|
42
|
+
md: 'h-5 px-2 text-[11px]',
|
|
43
|
+
lg: 'h-[22px] px-2.5 text-xs',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function TokenBadge({
|
|
47
|
+
colorVar,
|
|
48
|
+
children,
|
|
49
|
+
variant = 'soft',
|
|
50
|
+
dot = false,
|
|
51
|
+
icon,
|
|
52
|
+
size = 'md',
|
|
53
|
+
shape = 'tag',
|
|
54
|
+
uppercase = false,
|
|
55
|
+
className,
|
|
56
|
+
}: TokenBadgeProps) {
|
|
57
|
+
const solid = `rgb(var(${colorVar}))`
|
|
58
|
+
const style =
|
|
59
|
+
variant === 'outline'
|
|
60
|
+
? {
|
|
61
|
+
background: 'transparent',
|
|
62
|
+
color: solid,
|
|
63
|
+
boxShadow: `inset 0 0 0 1px rgb(var(${colorVar}) / 0.4)`,
|
|
64
|
+
}
|
|
65
|
+
: { background: `rgb(var(${colorVar}) / 0.12)`, color: solid }
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<span
|
|
69
|
+
className={cn(
|
|
70
|
+
'inline-flex w-fit shrink-0 items-center gap-1 font-bold whitespace-nowrap',
|
|
71
|
+
uppercase && 'uppercase tracking-[0.06em]',
|
|
72
|
+
SIZE_CLASS[size],
|
|
73
|
+
className,
|
|
74
|
+
)}
|
|
75
|
+
style={{
|
|
76
|
+
...style,
|
|
77
|
+
borderRadius:
|
|
78
|
+
shape === 'pill' ? 'var(--radius-full)' : 'var(--radius-xs)',
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{dot && !icon ? (
|
|
82
|
+
<span
|
|
83
|
+
aria-hidden
|
|
84
|
+
className="size-1.5 shrink-0 rounded-full"
|
|
85
|
+
style={{ background: 'currentColor' }}
|
|
86
|
+
/>
|
|
87
|
+
) : null}
|
|
88
|
+
{icon && <span className="flex shrink-0 items-center">{icon}</span>}
|
|
89
|
+
{children}
|
|
90
|
+
</span>
|
|
91
|
+
)
|
|
92
|
+
}
|