@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.
@@ -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
+ }
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  export { DataGrid } from './data-grid'
13
+ export { DataGridToolbar, type DataGridToolbarProps } from './toolbar'
13
14
  export { DataGridColumnOptionsMenu } from './column-options-menu'
14
15
  export { DrawerPanel } from './drawer-panel'
15
16
  export {
@@ -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
- // Thin, light scrollbars (light slate thumb on a transparent track).
38
- // Deliberately NOT styling ::-webkit-scrollbar that would force
39
- // classic always-on bars; leaving it unset preserves the platform's
40
- // auto-hiding overlay bars so they don't sit over the pinned action
41
- // column / content.
42
- scrollbarWidth: 'thin',
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
+ }