@lovett/ui 0.0.3 → 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/data-grid/index.ts +1 -0
- package/src/data-grid/table-elements.tsx +6 -7
- package/src/data-grid/toolbar.tsx +44 -0
- package/src/floating-status-bar.tsx +112 -0
- package/src/index.ts +13 -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
|
+
}
|
package/src/data-grid/index.ts
CHANGED
|
@@ -34,13 +34,12 @@ export const Table = forwardRef<
|
|
|
34
34
|
style={{
|
|
35
35
|
border: '1px solid rgb(var(--border))',
|
|
36
36
|
borderRadius: 'var(--radius-lg)',
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// column
|
|
42
|
-
|
|
43
|
-
scrollbarColor: 'rgba(100, 116, 139, 0.4) transparent',
|
|
37
|
+
// NOTE: intentionally NOT setting scrollbar-width / scrollbar-color.
|
|
38
|
+
// On macOS, any custom scrollbar property opts the element out of the
|
|
39
|
+
// platform's auto-hiding *overlay* scrollbars into *classic*
|
|
40
|
+
// always-visible ones — which left the vertical bar permanently
|
|
41
|
+
// covering the pinned action column. Native overlay bars are already
|
|
42
|
+
// thin + light AND auto-hide, so we leave them untouched.
|
|
44
43
|
}}
|
|
45
44
|
>
|
|
46
45
|
<table
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataGridToolbar — the standard layout band that sits directly above a
|
|
3
|
+
* DataGrid: a search field on the left and filter / column / view
|
|
4
|
+
* controls on the right (gray-ui "table toolbar" pattern). Collapses to
|
|
5
|
+
* a stacked column on narrow viewports.
|
|
6
|
+
*
|
|
7
|
+
* Purely a layout primitive — pass whatever search input + action
|
|
8
|
+
* buttons you like into the slots.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ReactNode } from 'react'
|
|
12
|
+
|
|
13
|
+
import { cn } from '../lib/utils'
|
|
14
|
+
|
|
15
|
+
export interface DataGridToolbarProps {
|
|
16
|
+
/** Left slot — typically a <SearchBar>. Stretches up to a comfortable
|
|
17
|
+
* reading width, full-width when stacked. */
|
|
18
|
+
search?: ReactNode
|
|
19
|
+
/** Right slot — filter / columns / view-toggle controls. */
|
|
20
|
+
children?: ReactNode
|
|
21
|
+
className?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function DataGridToolbar({
|
|
25
|
+
search,
|
|
26
|
+
children,
|
|
27
|
+
className,
|
|
28
|
+
}: DataGridToolbarProps) {
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
className={cn(
|
|
32
|
+
'flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between',
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
{search ? <div className="w-full sm:max-w-sm">{search}</div> : <span />}
|
|
37
|
+
{children ? (
|
|
38
|
+
<div className="flex flex-wrap items-center gap-2 sm:justify-end">
|
|
39
|
+
{children}
|
|
40
|
+
</div>
|
|
41
|
+
) : null}
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -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,
|
|
@@ -109,6 +120,7 @@ export {
|
|
|
109
120
|
} from './dropdown-menu'
|
|
110
121
|
export {
|
|
111
122
|
DataGrid,
|
|
123
|
+
DataGridToolbar,
|
|
112
124
|
DataGridColumnOptionsMenu,
|
|
113
125
|
DataGridDragOverlay,
|
|
114
126
|
DataGridDropIndicator,
|
|
@@ -117,6 +129,7 @@ export {
|
|
|
117
129
|
type DataGridProps,
|
|
118
130
|
type DataGridRowBase,
|
|
119
131
|
type DataGridSortState,
|
|
132
|
+
type DataGridToolbarProps,
|
|
120
133
|
type DataGridToolbarRenderProps,
|
|
121
134
|
type DataGridIcon,
|
|
122
135
|
type EditingCell,
|
|
@@ -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
|
+
}
|