@lovett/ui 0.0.4 → 0.0.6

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.
Files changed (59) hide show
  1. package/dist/index.d.ts +2659 -0
  2. package/dist/index.js +14451 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/styles.css +636 -0
  5. package/dist/tokens.css +700 -0
  6. package/package.json +44 -17
  7. package/src/__tests__/button.test.tsx +137 -0
  8. package/src/__tests__/card.test.tsx +103 -0
  9. package/src/__tests__/dead-render.test.tsx +117 -0
  10. package/src/__tests__/input.test.tsx +134 -0
  11. package/src/__tests__/modal.test.tsx +154 -0
  12. package/src/__tests__/page-shell.test.tsx +128 -0
  13. package/src/__tests__/setup.ts +43 -0
  14. package/src/__tests__/token-shape.test.ts +193 -0
  15. package/src/allocation-sparkbar.tsx +90 -0
  16. package/src/card.tsx +1 -1
  17. package/src/collapsible-card.tsx +85 -0
  18. package/src/data-grid/table-body.tsx +8 -1
  19. package/src/dropdown-menu.tsx +1 -1
  20. package/src/floating-status-bar.tsx +112 -0
  21. package/src/folder-tree-picker.tsx +5 -6
  22. package/src/frame-stack.tsx +27 -10
  23. package/src/hero-form-card.tsx +2 -2
  24. package/src/icons/brand.tsx +187 -0
  25. package/src/index.ts +43 -0
  26. package/src/lib/clipboard.ts +14 -0
  27. package/src/lib/color.ts +111 -0
  28. package/src/meta-cell.tsx +52 -0
  29. package/src/meta-previews/MetaFeedCarousel.tsx +1 -1
  30. package/src/meta-previews/MetaFeedPreview.tsx +1 -1
  31. package/src/microsoft-logo.tsx +33 -0
  32. package/src/modal.tsx +77 -6
  33. package/src/pill-button.tsx +23 -5
  34. package/src/profile-section.tsx +40 -9
  35. package/src/sortable-table.tsx +5 -1
  36. package/src/styles.css +74 -0
  37. package/src/tabs.tsx +4 -0
  38. package/src/tag-chip-input.tsx +1 -1
  39. package/src/theme-v2.css +466 -0
  40. package/src/token-badge.tsx +92 -0
  41. package/src/tokens.css +181 -60
  42. package/src/v2/README.md +208 -0
  43. package/src/v2/__demo__/showcase.tsx +1045 -0
  44. package/src/v2/action.tsx +91 -0
  45. package/src/v2/callout.tsx +76 -0
  46. package/src/v2/document-section.tsx +82 -0
  47. package/src/v2/document-shell.tsx +0 -0
  48. package/src/v2/field-row.tsx +113 -0
  49. package/src/v2/icons.tsx +165 -0
  50. package/src/v2/index.ts +147 -0
  51. package/src/v2/layout.tsx +293 -0
  52. package/src/v2/progress-track.tsx +89 -0
  53. package/src/v2/stat-tile.tsx +129 -0
  54. package/src/v2/states.tsx +271 -0
  55. package/src/v2/status-pill.tsx +74 -0
  56. package/src/v2/theme.css +1861 -0
  57. package/src/v2/timeline.tsx +81 -0
  58. package/src/v2/tokens.ts +228 -0
  59. package/src/value-chip.tsx +76 -0
@@ -0,0 +1,85 @@
1
+ // CollapsibleCard — a Card whose titled header toggles its body open/closed.
2
+ //
3
+ // Reviewing long specs means scrolling past sections you don't care about right
4
+ // now (a long Sitelinks list to reach Structured snippets, etc.). A collapsible
5
+ // titled card lets the reviewer fold sections away as they go. Built on the
6
+ // Card primitive; accessible (the title is a real button with aria-expanded)
7
+ // and honors prefers-reduced-motion.
8
+ //
9
+ // The collapse toggle is the chevron+title region only, so header `actions`
10
+ // (e.g. a "Suggest" button) stay independently clickable beside it — not nested
11
+ // inside the toggle button.
12
+ //
13
+ // Promoted from the SEM Spec lens per ADR-136 Decision 11 (the "2+ consumers ->
14
+ // promote" gate): the report tabs and the public share view both consume it.
15
+
16
+ import { useId, useState, type ReactNode } from 'react'
17
+ import { ChevronDown } from 'lucide-react'
18
+ import Card from './card'
19
+
20
+ export interface CollapsibleCardProps {
21
+ title: ReactNode
22
+ /** Optional leading icon (rendered before the title). */
23
+ icon?: ReactNode
24
+ /** Optional header content after the title (e.g. a count or subtitle). */
25
+ meta?: ReactNode
26
+ /** Right-aligned header controls, kept outside the collapse toggle. */
27
+ actions?: ReactNode
28
+ /** Start expanded (default) or collapsed. */
29
+ defaultOpen?: boolean
30
+ /** Body padding utility; pass '' for full-bleed bodies (e.g. tables). */
31
+ contentClassName?: string
32
+ children: ReactNode
33
+ }
34
+
35
+ export function CollapsibleCard({
36
+ title,
37
+ icon,
38
+ meta,
39
+ actions,
40
+ defaultOpen = true,
41
+ contentClassName = 'px-4 py-4',
42
+ children,
43
+ }: CollapsibleCardProps) {
44
+ const [open, setOpen] = useState(defaultOpen)
45
+ const bodyId = useId()
46
+
47
+ return (
48
+ <Card>
49
+ <div
50
+ className="flex items-center justify-between gap-3 px-4 py-3"
51
+ style={{
52
+ borderBottom: `1px solid ${open ? 'rgb(var(--border))' : 'transparent'}`,
53
+ }}
54
+ >
55
+ <button
56
+ type="button"
57
+ onClick={() => setOpen((o) => !o)}
58
+ aria-expanded={open}
59
+ aria-controls={bodyId}
60
+ className="flex min-w-0 flex-1 items-center gap-2 rounded-[var(--radius-sm)] py-0.5 text-left transition-colors focus-visible:outline-none focus-visible:[box-shadow:var(--ring-focus)]"
61
+ >
62
+ <ChevronDown
63
+ className="h-4 w-4 shrink-0 transition-transform duration-200 ease-out motion-reduce:transition-none"
64
+ style={{
65
+ color: 'rgb(var(--text-tertiary))',
66
+ transform: open ? 'rotate(0deg)' : 'rotate(-90deg)',
67
+ }}
68
+ aria-hidden="true"
69
+ />
70
+ {icon}
71
+ <span className="truncate text-[14px] font-semibold" style={{ color: 'rgb(var(--foreground))' }}>
72
+ {title}
73
+ </span>
74
+ {meta}
75
+ </button>
76
+ {actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
77
+ </div>
78
+ {open && (
79
+ <div id={bodyId} className={contentClassName}>
80
+ {children}
81
+ </div>
82
+ )}
83
+ </Card>
84
+ )
85
+ }
@@ -257,7 +257,14 @@ export function DataGridTableBody<
257
257
  return (
258
258
  <TableBody>
259
259
  {visibleRows.map((row, rowIndex) => (
260
- <TableRow key={row.id}>
260
+ // TableRow styles `data-[state=selected]`; without this attribute a
261
+ // checked row had no visual state at all — only the checkbox changed.
262
+ <TableRow
263
+ key={row.id}
264
+ {...(selectedRowIds.includes(row.id)
265
+ ? { 'data-state': 'selected' as const }
266
+ : {})}
267
+ >
261
268
  <TableCell
262
269
  className="h-10 px-0 text-center"
263
270
  style={{
@@ -276,7 +276,7 @@ export function DropdownMenuContent({
276
276
  'fixed z-[100] min-w-48 max-w-[min(28rem,calc(100vw-16px))]',
277
277
  'p-1.5 overflow-y-auto',
278
278
  'rounded-[var(--radius-lg)]',
279
- isPositioned && 'animate-in fade-in-0 zoom-in-95 duration-100',
279
+ isPositioned && 'ds-enter-pop',
280
280
  className,
281
281
  )}
282
282
  style={{
@@ -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
+ 'ds-enter-rise',
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
+ }
@@ -265,13 +265,12 @@ export function FolderTreePicker({
265
265
  aria-controls={open ? popoverId : undefined}
266
266
  disabled={disabled}
267
267
  onClick={() => setOpen((o) => !o)}
268
- className={cn(
269
- 'btn btn-secondary justify-between w-full',
270
- error && 'is-error',
271
- className,
272
- )}
268
+ className={cn('btn btn-secondary justify-between w-full', className)}
273
269
  style={{
274
- borderColor: error ? 'rgb(var(--ring-error))' : undefined,
270
+ /* --ring-error is a box-shadow LIST, not a colour; rgb(var(--ring-error))
271
+ is invalid CSS and gets dropped, which left this error state inert.
272
+ The colour token is --destructive (what .input-shell.is-error uses). */
273
+ borderColor: error ? 'rgb(var(--destructive))' : undefined,
275
274
  }}
276
275
  >
277
276
  <span className="inline-flex items-center gap-2 min-w-0">
@@ -1,13 +1,25 @@
1
1
  /**
2
2
  * FrameStack — slate outer-frame wrapper for the shell/tray pattern.
3
3
  *
4
- * Single purpose: wrap 2+ inner cards as ONE visual group. Renders a
5
- * slate-tinted outer frame (--card-frame-bg, --border-card) holding
6
- * the children with 10 px outer padding and 10 px gap between them.
4
+ * Single purpose: wrap 2+ inner cards as ONE visual group. Renders an
5
+ * outer frame (--surface-frame via --card-frame-bg, --border-card)
6
+ * holding the children with a --tray-inset outer padding and a slightly
7
+ * larger gap between them.
8
+ *
9
+ * The padding is the SAME token <Card> uses for its own tray inset, so a
10
+ * framed group and a framed card have an identical edge. It was a
11
+ * hardcoded 10 px, which made the group frame visibly chunkier than every
12
+ * card beside it — the two patterns are the same idea and should not
13
+ * measure differently.
14
+ *
15
+ * Padding smaller than the gap is deliberate, not an oversight: it is
16
+ * what the corpus kits measure (AI Agent's shell is pad 4 / gap 10), and
17
+ * it reads as panes seated in a tight frame rather than cards floating in
18
+ * a loose one.
7
19
  *
8
20
  * ┌─────────────────────────────────┐ ← <FrameStack>
9
- * │ ┌─────────────────────────────┐ │ slate bg (--card-frame-bg)
10
- * │ │ Card / SectionCard 01 │ │ 10 px padding, 10 px gap
21
+ * │ ┌─────────────────────────────┐ │ frame bg (--card-frame-bg)
22
+ * │ │ Card / SectionCard 01 │ │ --tray-inset padding, 10 px gap
11
23
  * │ └─────────────────────────────┘ │ 1 px --border-card outline
12
24
  * │ ┌─────────────────────────────┐ │ --radius-xl (20 px) outer
13
25
  * │ │ Card / SectionCard 02 │ │
@@ -28,9 +40,8 @@
28
40
  * elements as direct page-background children with `gap-3`. That's
29
41
  * "free-floating cards," not the shell/tray pattern. Wrap them.
30
42
  *
31
- * Lens-level lock: `apps/workspace/src/lenses/meta-creative/CLAUDE.md`
32
- * §"The shell/tray pattern" — the Meta Creative Builder uses this
33
- * primitive for the 3-section form.
43
+ * Consumers: the Social Spec builder (BuilderForm, TacticBuilder) and the
44
+ * Brand Profile section groups.
34
45
  *
35
46
  * Token discipline: internal tokens (--card-frame-bg, --border-card)
36
47
  * are touched HERE per ADR-006 D2 — primitives live under
@@ -45,10 +56,16 @@ export interface FrameStackProps extends HTMLAttributes<HTMLDivElement> {
45
56
  * prototype's spec. Override only with a token value
46
57
  * (e.g. `'var(--space-3)'` for 12 px) — never a literal. */
47
58
  gap?: string
59
+ /** Outer padding. Defaults to `--tray-inset`, the same inset <Card>
60
+ * uses, so a framed group and a framed card share one edge. */
61
+ padding?: string
48
62
  }
49
63
 
50
64
  export const FrameStack = forwardRef<HTMLDivElement, FrameStackProps>(
51
- function FrameStack({ className, style, gap = '10px', children, ...rest }, ref) {
65
+ function FrameStack(
66
+ { className, style, gap = '10px', padding = 'var(--tray-inset)', children, ...rest },
67
+ ref,
68
+ ) {
52
69
  return (
53
70
  <div
54
71
  ref={ref}
@@ -58,7 +75,7 @@ export const FrameStack = forwardRef<HTMLDivElement, FrameStackProps>(
58
75
  background: 'rgb(var(--card-frame-bg))',
59
76
  border: '1px solid rgb(var(--border-card))',
60
77
  borderRadius: 'var(--radius-xl)',
61
- padding: '10px',
78
+ padding,
62
79
  gap,
63
80
  ...style,
64
81
  }}
@@ -343,8 +343,8 @@ const BANNER_TONE: Record<
343
343
  icon: 'rgb(var(--warning))',
344
344
  },
345
345
  destructive: {
346
- bg: 'rgb(var(--surface-overlay-soft))',
347
- border: 'rgb(var(--ring-error))',
346
+ bg: 'rgb(var(--destructive-bg))',
347
+ border: 'rgb(var(--destructive))',
348
348
  icon: 'rgb(var(--destructive))',
349
349
  },
350
350
  }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Brand platform glyphs — real mono (currentColor) marks for the ad platforms
3
+ * the Social Spec builds for, replacing approximate Lucide stand-ins (Ghost for
4
+ * Snapchat, Home for Nextdoor, Music for TikTok, Megaphone for Meta). ADR-135.
5
+ *
6
+ * `@lovett/ui` is the one allowed home for bespoke SVG definitions (CLAUDE.md
7
+ * §2/§6) — same rationale as `MicrosoftLogo`. Path data is extracted from the
8
+ * owner's MIT-licensed fork github.com/edwinlov3tt/logos-apps (upstream: the
9
+ * ln-dev7 "logos" collection); each glyph is normalized to a single-colour
10
+ * `currentColor` silhouette so it adapts to its context (active pill = white,
11
+ * inactive tab = grey) — full-colour marks would clash inside the red pill.
12
+ * Extraction choices were picked by visual review; Facebook, Messenger and
13
+ * TikTok silhouettes are true path-boolean derivations of the official marks
14
+ * (disc⊖f, bubble⊖bolt, glitch-layer union — computed offline with paper.js,
15
+ * baked here as static path data).
16
+ *
17
+ * Marks are trademarks of their platforms — used nominatively to refer to the
18
+ * platforms themselves, exactly like the platform-chrome in the ad previews.
19
+ */
20
+
21
+ export interface BrandIconProps {
22
+ /** Square box size in px; the glyph letterboxes inside (non-square viewBoxes
23
+ * centre via preserveAspectRatio). Default 16 — a tab/affix glyph. */
24
+ size?: number
25
+ className?: string
26
+ }
27
+
28
+ /** Meta brand glyph (mono, currentColor). */
29
+ export function BrandMeta({ size = 16, className }: BrandIconProps) {
30
+ return (
31
+ <svg
32
+ width={size}
33
+ height={size}
34
+ viewBox="-5.12 -5.12 266.24 180.28"
35
+ className={className}
36
+ aria-hidden="true"
37
+ focusable="false"
38
+ >
39
+ <path d="M27.651 112.136c0 9.775 2.146 17.28 4.95 21.82c3.677 5.947 9.16 8.466 14.751 8.466c7.211 0 13.808-1.79 26.52-19.372c10.185-14.092 22.186-33.874 30.26-46.275l13.675-21.01c9.499-14.591 20.493-30.811 33.1-41.806C161.196 4.985 172.298 0 183.47 0c18.758 0 36.625 10.87 50.3 31.257C248.735 53.584 256 81.707 256 110.729c0 17.253-3.4 29.93-9.187 39.946c-5.591 9.686-16.488 19.363-34.818 19.363v-27.616c15.695 0 19.612-14.422 19.612-30.927c0-23.52-5.484-49.623-17.564-68.273c-8.574-13.23-19.684-21.313-31.907-21.313c-13.22 0-23.859 9.97-35.815 27.75c-6.356 9.445-12.882 20.956-20.208 33.944l-8.066 14.289c-16.203 28.728-20.307 35.271-28.408 46.07c-14.2 18.91-26.324 26.076-42.287 26.076c-18.935 0-30.91-8.2-38.325-20.556C2.973 139.413 0 126.202 0 111.148z" fill="currentColor" />
40
+ <path d="M21.802 33.206C34.48 13.666 52.774 0 73.757 0C85.91 0 97.99 3.597 110.605 13.897c13.798 11.261 28.505 29.805 46.853 60.368l6.58 10.967c15.881 26.459 24.917 40.07 30.205 46.49c6.802 8.243 11.565 10.7 17.752 10.7c15.695 0 19.612-14.422 19.612-30.927l24.393-.766c0 17.253-3.4 29.93-9.187 39.946c-5.591 9.686-16.488 19.363-34.818 19.363c-11.395 0-21.49-2.475-32.654-13.007c-8.582-8.083-18.615-22.443-26.334-35.352l-22.96-38.352C118.528 64.08 107.96 49.73 101.845 43.23c-6.578-6.988-15.036-15.428-28.532-15.428c-10.923 0-20.2 7.666-27.963 19.39z" fill="currentColor" />
41
+ <path d="M73.312 27.802c-10.923 0-20.2 7.666-27.963 19.39c-10.976 16.568-17.698 41.245-17.698 64.944c0 9.775 2.146 17.28 4.95 21.82L9.027 149.482C2.973 139.413 0 126.202 0 111.148C0 83.772 7.514 55.24 21.802 33.206C34.48 13.666 52.774 0 73.757 0z" fill="currentColor" />
42
+ </svg>
43
+ )
44
+ }
45
+
46
+ /** Facebook brand glyph (mono, currentColor). */
47
+ export function BrandFacebook({ size = 16, className }: BrandIconProps) {
48
+ return (
49
+ <svg
50
+ width={size}
51
+ height={size}
52
+ viewBox="-5.12 -5.12 266.24 264.69"
53
+ className={className}
54
+ aria-hidden="true"
55
+ focusable="false"
56
+ >
57
+ <path d="M256 128C256 57.308 198.692 0 128 0S0 57.308 0 128c0 63.888 46.808 116.843 108 126.445V165H75.5v-37H108V99.8c0-32.08 19.11-49.8 48.348-49.8C170.352 50 185 52.5 185 52.5V84h-16.14C152.959 84 148 93.867 148 103.99V128h35.5l-5.675 37H148v89.445c61.192-9.602 108-62.556 108-126.445" fill="currentColor" />
58
+ </svg>
59
+ )
60
+ }
61
+
62
+ /** Instagram brand glyph (mono, currentColor). */
63
+ export function BrandInstagram({ size = 16, className }: BrandIconProps) {
64
+ return (
65
+ <svg
66
+ width={size}
67
+ height={size}
68
+ viewBox="-5.12 -5.12 266.24 266.24"
69
+ className={className}
70
+ aria-hidden="true"
71
+ focusable="false"
72
+ >
73
+ <path d="M128 23.064c34.177 0 38.225.13 51.722.745c12.48.57 19.258 2.655 23.769 4.408c5.974 2.322 10.238 5.096 14.717 9.575s7.253 8.743 9.575 14.717c1.753 4.511 3.838 11.289 4.408 23.768c.615 13.498.745 17.546.745 51.723s-.13 38.226-.745 51.723c-.57 12.48-2.655 19.257-4.408 23.768c-2.322 5.974-5.096 10.239-9.575 14.718s-8.743 7.253-14.717 9.574c-4.511 1.753-11.289 3.839-23.769 4.408c-13.495.616-17.543.746-51.722.746s-38.228-.13-51.723-.746c-12.48-.57-19.257-2.655-23.768-4.408c-5.974-2.321-10.239-5.095-14.718-9.574c-4.479-4.48-7.253-8.744-9.574-14.718c-1.753-4.51-3.839-11.288-4.408-23.768c-.616-13.497-.746-17.545-.746-51.723s.13-38.225.746-51.722c.57-12.48 2.655-19.258 4.408-23.769c2.321-5.974 5.095-10.238 9.574-14.717c4.48-4.48 8.744-7.253 14.718-9.575c4.51-1.753 11.288-3.838 23.768-4.408c13.497-.615 17.545-.745 51.723-.745M128 0C93.237 0 88.878.147 75.226.77c-13.625.622-22.93 2.786-31.071 5.95c-8.418 3.271-15.556 7.648-22.672 14.764S9.991 35.738 6.72 44.155C3.555 52.297 1.392 61.602.77 75.226C.147 88.878 0 93.237 0 128s.147 39.122.77 52.774c.622 13.625 2.785 22.93 5.95 31.071c3.27 8.417 7.647 15.556 14.763 22.672s14.254 11.492 22.672 14.763c8.142 3.165 17.446 5.328 31.07 5.95c13.653.623 18.012.77 52.775.77s39.122-.147 52.774-.77c13.624-.622 22.929-2.785 31.07-5.95c8.418-3.27 15.556-7.647 22.672-14.763s11.493-14.254 14.764-22.672c3.164-8.142 5.328-17.446 5.95-31.07c.623-13.653.77-18.012.77-52.775s-.147-39.122-.77-52.774c-.622-13.624-2.786-22.929-5.95-31.07c-3.271-8.418-7.648-15.556-14.764-22.672S220.262 9.99 211.845 6.72c-8.142-3.164-17.447-5.328-31.071-5.95C167.122.147 162.763 0 128 0m0 62.27c-36.302 0-65.73 29.43-65.73 65.73s29.428 65.73 65.73 65.73c36.301 0 65.73-29.428 65.73-65.73c0-36.301-29.429-65.73-65.73-65.73m0 108.397c-23.564 0-42.667-19.103-42.667-42.667S104.436 85.333 128 85.333s42.667 19.103 42.667 42.667s-19.103 42.667-42.667 42.667m83.686-110.994c0 8.484-6.876 15.36-15.36 15.36s-15.36-6.876-15.36-15.36s6.877-15.36 15.36-15.36s15.36 6.877 15.36 15.36" fill="currentColor" />
74
+ </svg>
75
+ )
76
+ }
77
+
78
+ /** Messenger brand glyph (mono, currentColor). */
79
+ export function BrandMessenger({ size = 16, className }: BrandIconProps) {
80
+ return (
81
+ <svg
82
+ width={size}
83
+ height={size}
84
+ viewBox="-5.12 -5.12 266.24 266.24"
85
+ className={className}
86
+ aria-hidden="true"
87
+ focusable="false"
88
+ >
89
+ <path d="M128,0c-72.106,0 -128,52.818 -128,124.16c0,37.317 15.293,69.562 40.2,91.835c2.09,1.871 3.352,4.493 3.438,7.298l0.697,22.77c0.223,7.262 7.724,11.988 14.37,9.054l25.406,-11.217c2.15335,-0.94916 4.56829,-1.12613 6.837,-0.501c11.675,3.21 24.1,4.92 37.052,4.92c72.106,0 128,-52.818 128,-124.16c0,-71.342 -55.894,-124.159 -128,-124.159 M51.137,160.47l37.6,-59.653c5.98,-9.49 18.788,-11.853 27.762,-5.123l29.905,22.43c2.744,2.05808 6.52006,2.04706 9.252,-0.027l40.388,-30.652c5.39,-4.091 12.428,2.36 8.82,8.085l-37.6,59.654c-5.981,9.489 -18.79,11.852 -27.763,5.122l-29.906,-22.43c-2.74359,-2.05707 -6.51846,-2.04605 -9.25,0.027l-40.39,30.652c-5.39,4.09 -12.427,-2.36 -8.818,-8.085" fill="currentColor" />
90
+ </svg>
91
+ )
92
+ }
93
+
94
+ /** LinkedIn brand glyph (mono, currentColor). */
95
+ export function BrandLinkedIn({ size = 16, className }: BrandIconProps) {
96
+ return (
97
+ <svg
98
+ width={size}
99
+ height={size}
100
+ viewBox="1.44 -0.56 29.12 29.12"
101
+ className={className}
102
+ aria-hidden="true"
103
+ focusable="false"
104
+ >
105
+ <path d="M8.268 28H2.463V9.306h5.805zM5.362 6.756C3.506 6.756 2 5.218 2 3.362a3.362 3.362 0 0 1 6.724 0c0 1.856-1.506 3.394-3.362 3.394M29.994 28h-5.792v-9.1c0-2.169-.044-4.95-3.018-4.95c-3.018 0-3.481 2.356-3.481 4.794V28h-5.799V9.306h5.567v2.55h.081c.775-1.469 2.668-3.019 5.492-3.019c5.875 0 6.955 3.869 6.955 8.894V28z" fill="currentColor" />
106
+ </svg>
107
+ )
108
+ }
109
+
110
+ /** Nextdoor brand glyph (mono, currentColor). */
111
+ export function BrandNextdoor({ size = 16, className }: BrandIconProps) {
112
+ return (
113
+ <svg
114
+ width={size}
115
+ height={size}
116
+ viewBox="-1.28 5.29 66.56 53.4"
117
+ className={className}
118
+ aria-hidden="true"
119
+ focusable="false"
120
+ >
121
+ <path d="M0 25.993c.285-.085.56-.2.82-.343l9.538-5.894c.166-.125.325-.26.477-.4V6.708h10.168v6.524L32 6.574l32 19.6-5.265 8.622-5.475-3.338h-.172a.21.21 0 0 0 0 .114v25.58c-1.068.267-38.343.362-41.967.134V56.8q0-12.533 0-25.104l-.114-.23-5.76 3.32L3.1 31.373c-.916-1.488-1.908-2.976-2.766-4.464A1.91 1.91 0 0 0 0 26.508z" fill="currentColor" />
122
+ </svg>
123
+ )
124
+ }
125
+
126
+ /** Pinterest brand glyph (mono, currentColor). */
127
+ export function BrandPinterest({ size = 16, className }: BrandIconProps) {
128
+ return (
129
+ <svg
130
+ width={size}
131
+ height={size}
132
+ viewBox="3.38 -0.22 25.25 32.44"
133
+ className={className}
134
+ aria-hidden="true"
135
+ focusable="false"
136
+ >
137
+ <path d="M16.75.406C10.337.406 4 4.681 4 11.6c0 4.4 2.475 6.9 3.975 6.9c.619 0 .975-1.725.975-2.212c0-.581-1.481-1.819-1.481-4.238c0-5.025 3.825-8.588 8.775-8.588c4.256 0 7.406 2.419 7.406 6.863c0 3.319-1.331 9.544-5.644 9.544c-1.556 0-2.888-1.125-2.888-2.737c0-2.363 1.65-4.65 1.65-7.088c0-4.137-5.869-3.387-5.869 1.613c0 1.05.131 2.212.6 3.169c-.863 3.713-2.625 9.244-2.625 13.069c0 1.181.169 2.344.281 3.525c.212.238.106.213.431.094c3.15-4.313 3.038-5.156 4.463-10.8c.769 1.463 2.756 2.25 4.331 2.25c6.637 0 9.619-6.469 9.619-12.3c0-6.206-5.363-10.256-11.25-10.256z" fill="currentColor" />
138
+ </svg>
139
+ )
140
+ }
141
+
142
+ /** Snapchat brand glyph (mono, currentColor). */
143
+ export function BrandSnapchat({ size = 16, className }: BrandIconProps) {
144
+ return (
145
+ <svg
146
+ width={size}
147
+ height={size}
148
+ viewBox="208.68 112.18 391.9 368.85"
149
+ className={className}
150
+ aria-hidden="true"
151
+ focusable="false"
152
+ >
153
+ <path d="M407.001,473.488c-1.068,0-2.087-0.039-2.862-0.076c-0.615,0.053-1.25,0.076-1.886,0.076 c-22.437,0-37.439-10.607-50.678-19.973c-9.489-6.703-18.438-13.031-28.922-14.775c-5.149-0.854-10.271-1.287-15.22-1.287 c-8.917,0-15.964,1.383-21.109,2.389c-3.166,0.617-5.896,1.148-8.006,1.148c-2.21,0-4.895-0.49-6.014-4.311 c-0.887-3.014-1.523-5.934-2.137-8.746c-1.536-7.027-2.65-11.316-5.281-11.723c-28.141-4.342-44.768-10.738-48.08-18.484 c-0.347-0.814-0.541-1.633-0.584-2.443c-0.129-2.309,1.501-4.334,3.777-4.711c22.348-3.68,42.219-15.492,59.064-35.119 c13.049-15.195,19.457-29.713,20.145-31.316c0.03-0.072,0.065-0.148,0.101-0.217c3.247-6.588,3.893-12.281,1.926-16.916 c-3.626-8.551-15.635-12.361-23.58-14.882c-1.976-0.625-3.845-1.217-5.334-1.808c-7.043-2.782-18.626-8.66-17.083-16.773 c1.124-5.916,8.949-10.036,15.273-10.036c1.756,0,3.312,0.308,4.622,0.923c7.146,3.348,13.575,5.045,19.104,5.045 c6.876,0,10.197-2.618,11-3.362c-0.198-3.668-0.44-7.546-0.674-11.214c0-0.004-0.005-0.048-0.005-0.048 c-1.614-25.675-3.627-57.627,4.546-75.95c24.462-54.847,76.339-59.112,91.651-59.112c0.408,0,6.674-0.062,6.674-0.062 c0.283-0.005,0.59-0.009,0.908-0.009c15.354,0,67.339,4.27,91.816,59.15c8.173,18.335,6.158,50.314,4.539,76.016l-0.076,1.23 c-0.222,3.49-0.427,6.793-0.6,9.995c0.756,0.696,3.795,3.096,9.978,3.339c5.271-0.202,11.328-1.891,17.998-5.014 c2.062-0.968,4.345-1.169,5.895-1.169c2.343,0,4.727,0.456,6.714,1.285l0.106,0.041c5.66,2.009,9.367,6.024,9.447,10.242 c0.071,3.932-2.851,9.809-17.223,15.485c-1.472,0.583-3.35,1.179-5.334,1.808c-7.952,2.524-19.951,6.332-23.577,14.878 c-1.97,4.635-1.322,10.326,1.926,16.912c0.036,0.072,0.067,0.145,0.102,0.221c1,2.344,25.205,57.535,79.209,66.432 c2.275,0.379,3.908,2.406,3.778,4.711c-0.048,0.828-0.248,1.656-0.598,2.465c-3.289,7.703-19.915,14.09-48.064,18.438 c-2.642,0.408-3.755,4.678-5.277,11.668c-0.63,2.887-1.271,5.717-2.146,8.691c-0.819,2.797-2.641,4.164-5.567,4.164h-0.441 c-1.905,0-4.604-0.346-8.008-1.012c-5.95-1.158-12.623-2.236-21.109-2.236c-4.948,0-10.069,0.434-15.224,1.287 c-10.473,1.744-19.421,8.062-28.893,14.758C444.443,462.88,429.436,473.488,407.001,473.488" fill="currentColor" />
154
+ </svg>
155
+ )
156
+ }
157
+
158
+ /** TikTok brand glyph (mono, currentColor). */
159
+ export function BrandTikTok({ size = 16, className }: BrandIconProps) {
160
+ return (
161
+ <svg
162
+ width={size}
163
+ height={size}
164
+ viewBox="-5.79 -5.79 267.56 300.84"
165
+ className={className}
166
+ aria-hidden="true"
167
+ focusable="false"
168
+ >
169
+ <path d="M189.722,104.42243l0,96.99357c0,48.525 -39.357,87.857 -87.905,87.857c-24.62405,0 -46.88248,-10.1174 -62.84421,-26.42813c-0.00093,-0.00062 -0.00186,-0.00125 -0.00279,-0.00187c-0.00254,-0.00259 -0.00508,-0.00519 -0.00761,-0.00778c-23.49917,-15.76977 -38.96239,-42.57494 -38.96239,-72.99022c0,-48.518 39.355,-87.852 87.905,-87.852c4.03423,-0.001 8.06385,0.2723 12.061,0.818l0,10.76898c4.64967,-0.09894 9.30133,0.17007 13.911,0.80102v48.593c-3.94487,-1.25017 -8.05877,-1.88528 -12.197,-1.883c-22.209,0 -40.21,17.994 -40.21,40.186c-0.00942,6.11418 1.38223,12.11209 4.025,17.54894l-17.941,-29.11894c0,-6.79366 1.68644,-13.19354 4.66379,-18.8042c-2.97797,5.61107 -4.66479,12.01139 -4.66479,18.8052c0,15.692 8.998,29.28 22.118,35.898c0.00103,0.00136 0.00207,0.00271 0.0031,0.00407c5.60932,2.82644 11.80469,4.29426 18.0859,4.28493c22.162,0 40.132,-17.92 40.208,-40.05v-189.845h47.834v6.113c0.1689,1.82808 0.41177,3.64857 0.728,5.457l-34.643,0.001v189.844c-0.078,22.132 -18.05,40.05 -40.21,40.05c-5.67604,0 -11.07698,-1.17525 -15.97293,-3.29552c4.89621,2.12093 10.2975,3.29652 15.97393,3.29652c22.161,0 40.131,-17.92 40.21,-40.052v-189.843l47.832,0v6.113c1.356,14.892 7.624,28.362 17.17,38.785h-0.001c10.55395,6.58342 22.74707,10.06504 35.186,10.047v10.116c4.57579,0.96983 9.24056,1.45782 13.918,1.456v47.53c-24.71212,0 -47.59337,-7.85144 -66.271,-21.19557zM99.964,151.40168c0.00033,0.00011 0.00067,0.00021 0.001,0.00032l0,-37.82098c-0.00033,0.00001 -0.00067,0.00001 -0.001,0.00002zM242.075,114.045v-0.001c-24.712,0 -47.589,-7.851 -66.272,-21.195v0.001c18.682,13.344 41.561,21.195 66.272,21.195zM69.673,225.607c-1.62409,-2.12792 -3.02095,-4.40072 -4.178,-6.78106zM228.74787,72.27412c3.67381,1.65002 7.52454,2.97698 11.51604,3.94463c-3.9686,-0.96405 -7.82207,-2.2874 -11.51604,-3.94463z" fill="currentColor" />
170
+ </svg>
171
+ )
172
+ }
173
+
174
+ /** Keyed map for registry-style resolution (e.g. the Social Spec tactic icons). */
175
+ export const BRAND_ICONS = {
176
+ meta: BrandMeta,
177
+ facebook: BrandFacebook,
178
+ instagram: BrandInstagram,
179
+ messenger: BrandMessenger,
180
+ linkedin: BrandLinkedIn,
181
+ nextdoor: BrandNextdoor,
182
+ pinterest: BrandPinterest,
183
+ snapchat: BrandSnapchat,
184
+ tiktok: BrandTikTok,
185
+ } as const
186
+
187
+ export type BrandIconKey = keyof typeof BRAND_ICONS
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  // `@lovett/ui/styles.css` — component CSS the primitives render through
10
10
 
11
11
  export { default as Card, IconTile, TagRow } from './card'
12
+ export { CollapsibleCard, type CollapsibleCardProps } from './collapsible-card'
12
13
  export { Button, type ButtonProps, type ButtonVariant, type ButtonSize, type ButtonShape } from './button'
13
14
  export { ButtonGroup, type ButtonGroupProps } from './button-group'
14
15
  export { Input, type InputProps, type InputSize } from './input'
@@ -23,6 +24,21 @@ export { default as BrandLogoTile, type BrandLogoTileProps, type BrandLogoTileSi
23
24
  export { default as ProfileSection, type ProfileSectionProps } from './profile-section'
24
25
  export { default as ChipNav, type ChipNavItem } from './chip-nav'
25
26
  export { default as CompletionRing, type CompletionRingProps } from './completion-ring'
27
+ export { default as MicrosoftLogo, type MicrosoftLogoProps } from './microsoft-logo'
28
+ export {
29
+ BrandMeta,
30
+ BrandFacebook,
31
+ BrandInstagram,
32
+ BrandMessenger,
33
+ BrandLinkedIn,
34
+ BrandNextdoor,
35
+ BrandPinterest,
36
+ BrandSnapchat,
37
+ BrandTikTok,
38
+ BRAND_ICONS,
39
+ type BrandIconKey,
40
+ type BrandIconProps,
41
+ } from './icons/brand'
26
42
  export { default as Choropleth, type ChoroplethProps } from './choropleth'
27
43
  export {
28
44
  default as PageShell,
@@ -76,6 +92,33 @@ export { MetricCard, type MetricCardProps, type MetricCardTone } from './metric-
76
92
  export { ProgressBar, type ProgressBarProps } from './progress-bar'
77
93
  export { StepLoader, type StepLoaderProps, type StepLoaderStep } from './step-loader'
78
94
  export { Badge, type BadgeProps, type BadgeTone } from './badge'
95
+ export { TokenBadge, type TokenBadgeProps } from './token-badge'
96
+ // v2 language atoms — a literal value in a well, and a labelled fact in a
97
+ // well. Both were hand-rolled per lens before; see each file's header.
98
+ export { ValueChip, type ValueChipProps } from './value-chip'
99
+ export { MetaCell, type MetaCellProps } from './meta-cell'
100
+ export { copyText } from './lib/clipboard'
101
+ export {
102
+ hexToRgbTuple,
103
+ rgbString,
104
+ rgbCss,
105
+ hslString,
106
+ relativeLuminance,
107
+ contrastRatio,
108
+ bestTextOn,
109
+ wcagBadges,
110
+ type RgbTuple,
111
+ type WcagBadge,
112
+ } from './lib/color'
113
+ export {
114
+ AllocationSparkbar,
115
+ type AllocationSparkbarProps,
116
+ } from './allocation-sparkbar'
117
+ export {
118
+ FloatingStatusBar,
119
+ type FloatingStatusBarProps,
120
+ type FloatingStatusTone,
121
+ } from './floating-status-bar'
79
122
  export { StatsGrid, type StatsGridProps } from './stats-grid'
80
123
  export {
81
124
  SortableTable,
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Copy-with-confirmation. Promoted from the extension's local helper so
3
+ * the toast wording is identical everywhere a value is copyable.
4
+ */
5
+ import { toast } from '../toast'
6
+
7
+ export async function copyText(value: string, label?: string): Promise<void> {
8
+ try {
9
+ await navigator.clipboard.writeText(value)
10
+ toast.success(label ? `Copied ${label}` : `Copied ${value}`)
11
+ } catch {
12
+ toast.error('Copy failed')
13
+ }
14
+ }