@lovett/ui 0.0.8 → 0.0.10

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/src/v2/action.tsx DELETED
@@ -1,91 +0,0 @@
1
- /**
2
- * @lovett/ui v2 — Action
3
- *
4
- * The v2 button. It exists because every other v2 primitive that offers
5
- * an affordance (Callout, EmptyState, ErrorState, DocumentShell header,
6
- * Section head) needs one, and pulling in the v1 `Button` would drag
7
- * v1's whole stylesheet into a v2 page.
8
- *
9
- * The rule this component is built to enforce: **exactly one `primary`
10
- * per view**. The accent earns its impact from scarcity; a screen with
11
- * three filled red buttons has no primary action at all. Everything that
12
- * is not *the* action is `secondary`, `ghost`, or `danger`.
13
- *
14
- * Size is invariant across every state. Loading keeps the label in the
15
- * DOM (holding the width) and hides it behind a spinner; disabled only
16
- * changes paint. The button never resizes under the pointer.
17
- */
18
-
19
- import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react'
20
- import { cn } from '../lib/utils'
21
- import { space } from './tokens'
22
-
23
- export type ActionVariant = 'primary' | 'secondary' | 'ghost' | 'danger'
24
- export type ActionSize = 'sm' | 'md' | 'lg'
25
-
26
- export interface ActionProps
27
- extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
28
- variant?: ActionVariant
29
- size?: ActionSize
30
- /** Shows the spinner and blocks interaction. The box does not resize. */
31
- loading?: boolean
32
- /** Leading icon. Stroke weight should match the label weight (1.5). */
33
- icon?: ReactNode
34
- /** Trailing icon. */
35
- trailingIcon?: ReactNode
36
- children?: ReactNode
37
- /**
38
- * Required when the button has no visible label — an icon-only control
39
- * with no accessible name is invisible to a screen reader.
40
- */
41
- 'aria-label'?: string
42
- }
43
-
44
- export const Action = forwardRef<HTMLButtonElement, ActionProps>(function Action(
45
- {
46
- variant = 'secondary',
47
- size = 'md',
48
- loading = false,
49
- icon,
50
- trailingIcon,
51
- disabled,
52
- className,
53
- children,
54
- type = 'button',
55
- ...rest
56
- },
57
- ref,
58
- ) {
59
- const iconOnly = children === undefined || children === null || children === ''
60
-
61
- return (
62
- <button
63
- ref={ref}
64
- type={type}
65
- data-variant={variant}
66
- data-size={size}
67
- data-loading={loading ? 'true' : undefined}
68
- data-icon-only={iconOnly ? 'true' : undefined}
69
- disabled={disabled || loading}
70
- aria-busy={loading || undefined}
71
- className={cn('lv-action', className)}
72
- {...rest}
73
- >
74
- {/* Wrapped together so the spinner can cover label AND icons
75
- without any of them leaving the layout. */}
76
- <span
77
- className="lv-action-label lv-inline"
78
- style={{ alignItems: 'center', gap: space(2) }}
79
- >
80
- {icon}
81
- {children}
82
- {trailingIcon}
83
- </span>
84
- {loading && (
85
- <span className="lv-action-spinner">
86
- <span className="lv-spin" />
87
- </span>
88
- )}
89
- </button>
90
- )
91
- })
@@ -1,76 +0,0 @@
1
- /**
2
- * @lovett/ui v2 — Callout
3
- *
4
- * An aside that carries a state: info, success, warn, danger.
5
- *
6
- * Every tone ships with a default icon and every icon has a distinct
7
- * silhouette (circle / circled-check / triangle / octagon), so the tone
8
- * survives greyscale, colour-blindness, and a low-quality projector.
9
- * Colour is never the only signal — that is not a nicety here, it is the
10
- * reason `danger` is burnt orange rather than red: the house accent is
11
- * red, and two reds on one page cannot be told apart.
12
- *
13
- * The tone is drawn as a 2px leading rule plus a soft tinted fill, not a
14
- * full border. A boxed callout reads as a form field; a ruled one reads
15
- * as an aside, which is what it is.
16
- */
17
-
18
- import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
19
- import { cn } from '../lib/utils'
20
- import { IconDanger, IconInfo, IconSuccess, IconWarning } from './icons'
21
-
22
- export type CalloutTone = 'info' | 'success' | 'warn' | 'danger'
23
-
24
- const TONE_ICON: Readonly<Record<CalloutTone, ReactNode>> = {
25
- info: <IconInfo size={18} />,
26
- success: <IconSuccess size={18} />,
27
- warn: <IconWarning size={18} />,
28
- danger: <IconDanger size={18} />,
29
- }
30
-
31
- /** Screen-reader prefix. The tone must reach a non-visual reader too. */
32
- const TONE_WORD: Readonly<Record<CalloutTone, string>> = {
33
- info: 'Note',
34
- success: 'Success',
35
- warn: 'Warning',
36
- danger: 'Error',
37
- }
38
-
39
- export interface CalloutProps
40
- extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
41
- tone?: CalloutTone
42
- title?: ReactNode
43
- /**
44
- * Replace the default tone icon. Keep a distinct silhouette — the
45
- * icon, not the hue, is what makes the tone legible.
46
- */
47
- icon?: ReactNode
48
- /** Trailing controls. Ghost or secondary; never the view's primary. */
49
- actions?: ReactNode
50
- children?: ReactNode
51
- }
52
-
53
- export const Callout = forwardRef<HTMLDivElement, CalloutProps>(function Callout(
54
- { tone = 'info', title, icon, actions, className, children, ...rest },
55
- ref,
56
- ) {
57
- return (
58
- <div
59
- ref={ref}
60
- data-tone={tone}
61
- // `alert` for the two tones a reader must not miss; `note` for the
62
- // two they can read in document order.
63
- role={tone === 'danger' || tone === 'warn' ? 'alert' : 'note'}
64
- className={cn('lv-callout', className)}
65
- {...rest}
66
- >
67
- <span className="lv-callout-icon">{icon ?? TONE_ICON[tone]}</span>
68
- <div style={{ minWidth: 0 }}>
69
- <span className="lv-sr-only">{TONE_WORD[tone]}: </span>
70
- {title && <p className="lv-callout-title">{title}</p>}
71
- {children && <div className="lv-callout-body">{children}</div>}
72
- {actions && <div className="lv-callout-actions">{actions}</div>}
73
- </div>
74
- </div>
75
- )
76
- })
@@ -1,82 +0,0 @@
1
- /**
2
- * @lovett/ui v2 — DocumentSection
3
- *
4
- * A headed, anchored region inside a `DocumentShell`. The `id` is both
5
- * the URL fragment and the key the shell's scroll-spy matches on, so it
6
- * must equal the `id` of the matching entry in `sections`.
7
- *
8
- * `tabIndex={-1}` is deliberate: the shell moves focus here when a rail
9
- * item is clicked, so keyboard users land in the same place the scroll
10
- * did. It never enters the tab order on its own.
11
- */
12
-
13
- import {
14
- forwardRef,
15
- type HTMLAttributes,
16
- type ReactNode,
17
- } from 'react'
18
- import { cn } from '../lib/utils'
19
- import { space } from './tokens'
20
-
21
- export interface DocumentSectionProps
22
- extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
23
- /** Anchor id. Must match the `DocumentShell` nav entry. */
24
- id: string
25
- title: ReactNode
26
- description?: ReactNode
27
- /**
28
- * Trailing controls for this section. Secondary/ghost only — the one
29
- * primary action on a document belongs in the shell header.
30
- */
31
- actions?: ReactNode
32
- children?: ReactNode
33
- }
34
-
35
- export const DocumentSection = forwardRef<HTMLElement, DocumentSectionProps>(
36
- function DocumentSection(
37
- { id, title, description, actions, className, children, ...rest },
38
- ref,
39
- ) {
40
- const headingId = `${id}-heading`
41
- return (
42
- <section
43
- ref={ref}
44
- id={id}
45
- data-lv-section={id}
46
- tabIndex={-1}
47
- aria-labelledby={headingId}
48
- className={cn('lv-docsection', className)}
49
- {...rest}
50
- >
51
- <div className="lv-docsection-head">
52
- <div style={{ minWidth: 0 }}>
53
- <h2 id={headingId} className="lv-docsection-title">
54
- {title}
55
- </h2>
56
- {description && <p className="lv-docsection-desc">{description}</p>}
57
- </div>
58
- {/* Always rendered. A section that gains an action later must
59
- not change height when it does. */}
60
- <div
61
- className="lv-inline"
62
- style={{ gap: space(2), alignItems: 'center', flex: '0 0 auto' }}
63
- >
64
- {actions}
65
- </div>
66
- </div>
67
- <div className="lv-docsection-body">{children}</div>
68
- </section>
69
- )
70
- },
71
- )
72
-
73
- /**
74
- * Long-form body copy inside a document. Capped at ~68 characters so the
75
- * eye can find the next line, and `text-wrap: pretty` so the last line
76
- * never strands a single word.
77
- */
78
- export const Prose = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
79
- function Prose({ className, ...rest }, ref) {
80
- return <div ref={ref} className={cn('lv-prose', className)} {...rest} />
81
- },
82
- )
Binary file
@@ -1,113 +0,0 @@
1
- /**
2
- * @lovett/ui v2 — FieldRow / DefinitionList
3
- *
4
- * Label/value pairs on a shared alignment grid. Every row in the app
5
- * uses the same label-column width token (`--lv-dl-label-w`), so labels
6
- * and values line up across unrelated components — a section heading's
7
- * left edge, the field labels below it, and the footer text all sit on
8
- * the same column. That shared edge is most of what makes a dense page
9
- * read as designed rather than assembled.
10
- *
11
- * Below 640px the grid stacks instead of truncating: the value is what
12
- * the reader came for, and squeezing it to fit a label column is the
13
- * wrong trade.
14
- *
15
- * A `<div>` grouping `<dt>`/`<dd>` inside a `<dl>` is valid HTML, which
16
- * is what lets each row be its own grid while still sharing one column
17
- * width.
18
- */
19
-
20
- import {
21
- forwardRef,
22
- type CSSProperties,
23
- type HTMLAttributes,
24
- type ReactNode,
25
- } from 'react'
26
- import { cn } from '../lib/utils'
27
-
28
- export interface DefinitionListProps extends HTMLAttributes<HTMLDListElement> {
29
- /**
30
- * Draw hairlines between rows. Off by default — spacing groups these
31
- * perfectly well, and a rule per row is the most common way a clean
32
- * detail panel turns into a spreadsheet.
33
- */
34
- dividers?: boolean
35
- /** Override the shared label column width (px). Applies to every row
36
- * inside this list. */
37
- labelWidth?: number
38
- children?: ReactNode
39
- }
40
-
41
- export const DefinitionList = forwardRef<HTMLDListElement, DefinitionListProps>(
42
- function DefinitionList({ dividers, labelWidth, className, style, ...rest }, ref) {
43
- return (
44
- <dl
45
- ref={ref}
46
- data-dividers={dividers ? 'true' : undefined}
47
- className={cn('lv-dl', className)}
48
- style={
49
- labelWidth !== undefined
50
- ? // A custom property is the only way to reach every nested
51
- // row without prop-drilling; the cast is confined here.
52
- ({ ...style, '--lv-dl-label-w': `${labelWidth}px` } as CSSProperties)
53
- : style
54
- }
55
- {...rest}
56
- />
57
- )
58
- },
59
- )
60
-
61
- export interface FieldRowProps extends HTMLAttributes<HTMLDivElement> {
62
- label: ReactNode
63
- /**
64
- * The value. When `undefined` or `null` the row still renders at full
65
- * height with the `emptyText` placeholder — a missing field must not
66
- * change the shape of the panel around it.
67
- */
68
- value?: ReactNode
69
- /** Shown when `value` is nullish. */
70
- emptyText?: string
71
- /** Small note under the value. */
72
- hint?: ReactNode
73
- /** Force the stacked layout regardless of width. */
74
- layout?: 'inline' | 'stacked'
75
- /** Render the value with tabular figures. Use for anything comparable
76
- * down a column — IDs, counts, dates, money. */
77
- numeric?: boolean
78
- }
79
-
80
- export const FieldRow = forwardRef<HTMLDivElement, FieldRowProps>(
81
- function FieldRow(
82
- {
83
- label,
84
- value,
85
- emptyText = '—',
86
- hint,
87
- layout = 'inline',
88
- numeric,
89
- className,
90
- ...rest
91
- },
92
- ref,
93
- ) {
94
- const isEmpty = value === undefined || value === null || value === ''
95
- return (
96
- <div
97
- ref={ref}
98
- data-layout={layout}
99
- className={cn('lv-field-row', className)}
100
- {...rest}
101
- >
102
- <dt className="lv-field-label">{label}</dt>
103
- <dd
104
- className={cn('lv-field-value', numeric && 'lv-num')}
105
- data-empty={isEmpty ? 'true' : undefined}
106
- >
107
- {isEmpty ? emptyText : value}
108
- {hint && <span className="lv-field-hint">{hint}</span>}
109
- </dd>
110
- </div>
111
- )
112
- },
113
- )
package/src/v2/icons.tsx DELETED
@@ -1,165 +0,0 @@
1
- /**
2
- * @lovett/ui v2 — icons.tsx
3
- *
4
- * A deliberately tiny inline icon set. Two reasons it exists rather than
5
- * reaching for `lucide-react`:
6
- *
7
- * 1. Semantic components (Callout, StatusPill, ErrorState) MUST carry an
8
- * icon — colour is never allowed to be the only signal for a state.
9
- * That guarantee cannot depend on the consumer having installed an
10
- * icon library or on which one they picked.
11
- * 2. Stroke weight is a typography decision. These are all authored at
12
- * `1.5` so they sit correctly beside regular/medium UI text; a
13
- * hairline icon beside semibold text reads as a rendering bug.
14
- *
15
- * Every icon is one SVG using `currentColor`. States (hover, active,
16
- * disabled) come from CSS colour and opacity — never a second asset.
17
- * Consumers who prefer their own set can pass an `icon` prop anywhere
18
- * one of these is used as a default.
19
- */
20
-
21
- import type { SVGProps } from 'react'
22
-
23
- export interface IconProps extends Omit<SVGProps<SVGSVGElement>, 'children'> {
24
- /** Square edge length in px. Defaults to 16 — the size that pairs with
25
- * `--lv-text-sm`. */
26
- size?: number
27
- }
28
-
29
- function iconAttrs({ size = 16, ...rest }: IconProps): SVGProps<SVGSVGElement> {
30
- return {
31
- width: size,
32
- height: size,
33
- viewBox: '0 0 24 24',
34
- fill: 'none',
35
- stroke: 'currentColor',
36
- strokeWidth: 1.5,
37
- strokeLinecap: 'round',
38
- strokeLinejoin: 'round',
39
- 'aria-hidden': true,
40
- focusable: false,
41
- ...rest,
42
- }
43
- }
44
-
45
- /** Info — the neutral aside. */
46
- export function IconInfo(props: IconProps) {
47
- return (
48
- <svg {...iconAttrs(props)}>
49
- <circle cx="12" cy="12" r="9" />
50
- <path d="M12 11v5" />
51
- <path d="M12 7.75h.01" />
52
- </svg>
53
- )
54
- }
55
-
56
- /** Success — a check inside a circle, not a bare tick. The enclosure is
57
- * what makes it read as a *state* rather than a selection. */
58
- export function IconSuccess(props: IconProps) {
59
- return (
60
- <svg {...iconAttrs(props)}>
61
- <circle cx="12" cy="12" r="9" />
62
- <path d="m8.5 12.2 2.4 2.4 4.6-5" />
63
- </svg>
64
- )
65
- }
66
-
67
- /** Warning — triangle. Shape carries the meaning even in greyscale. */
68
- export function IconWarning(props: IconProps) {
69
- return (
70
- <svg {...iconAttrs(props)}>
71
- <path d="M10.3 4.3 2.9 17a2 2 0 0 0 1.7 3h14.8a2 2 0 0 0 1.7-3L13.7 4.3a2 2 0 0 0-3.4 0Z" />
72
- <path d="M12 9.5v4" />
73
- <path d="M12 17h.01" />
74
- </svg>
75
- )
76
- }
77
-
78
- /** Danger — octagon. Distinct silhouette from the warning triangle,
79
- * which matters more than the hue difference between them. */
80
- export function IconDanger(props: IconProps) {
81
- return (
82
- <svg {...iconAttrs(props)}>
83
- <path d="M8.6 2.5h6.8l4.8 4.8v6.8l-4.8 4.8H8.6l-4.8-4.8V7.3z" />
84
- <path d="M12 8v4.5" />
85
- <path d="M12 16h.01" />
86
- </svg>
87
- )
88
- }
89
-
90
- /** Empty — an open, unfilled container. */
91
- export function IconEmpty(props: IconProps) {
92
- return (
93
- <svg {...iconAttrs(props)}>
94
- <path d="M3 9.5 5.2 4.8A2 2 0 0 1 7 3.7h10a2 2 0 0 1 1.8 1.1L21 9.5" />
95
- <path d="M3 9.5h5l1.4 2.8h5.2L16 9.5h5v8a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 3 17.5z" />
96
- </svg>
97
- )
98
- }
99
-
100
- /** Retry. */
101
- export function IconRetry(props: IconProps) {
102
- return (
103
- <svg {...iconAttrs(props)}>
104
- <path d="M20 11.5A8 8 0 1 1 17.7 6" />
105
- <path d="M20.5 3.5V9H15" />
106
- </svg>
107
- )
108
- }
109
-
110
- /** Check — bare tick, for completed timeline markers. */
111
- export function IconCheck(props: IconProps) {
112
- return (
113
- <svg {...iconAttrs(props)}>
114
- <path d="m5 12.5 4.5 4.5L19 7" />
115
- </svg>
116
- )
117
- }
118
-
119
- /** Chevron, trailing direction. Flip with `scaleX(-1)` under RTL. */
120
- export function IconChevronRight(props: IconProps) {
121
- return (
122
- <svg {...iconAttrs(props)}>
123
- <path d="m9.5 5.5 6.5 6.5-6.5 6.5" />
124
- </svg>
125
- )
126
- }
127
-
128
- /** Arrow up — a rising delta. Paired with a sign in the label, so the
129
- * direction is never carried by colour alone. */
130
- export function IconArrowUp(props: IconProps) {
131
- return (
132
- <svg {...iconAttrs(props)}>
133
- <path d="M12 19.5V5" />
134
- <path d="m5.5 11.5 6.5-6.5 6.5 6.5" />
135
- </svg>
136
- )
137
- }
138
-
139
- /** Arrow down — a falling delta. */
140
- export function IconArrowDown(props: IconProps) {
141
- return (
142
- <svg {...iconAttrs(props)}>
143
- <path d="M12 4.5V19" />
144
- <path d="m5.5 12.5 6.5 6.5 6.5-6.5" />
145
- </svg>
146
- )
147
- }
148
-
149
- /** Flat — no change. */
150
- export function IconFlat(props: IconProps) {
151
- return (
152
- <svg {...iconAttrs(props)}>
153
- <path d="M5 12h14" />
154
- </svg>
155
- )
156
- }
157
-
158
- /** Dot — the neutral timeline marker. */
159
- export function IconDot(props: IconProps) {
160
- return (
161
- <svg {...iconAttrs(props)} fill="currentColor" stroke="none">
162
- <circle cx="12" cy="12" r="4" />
163
- </svg>
164
- )
165
- }
package/src/v2/index.ts DELETED
@@ -1,147 +0,0 @@
1
- /**
2
- * @lovett/ui v2 — public surface
3
- *
4
- * import { DocumentShell, StatTile, Callout } from '@lovett/ui/v2'
5
- * import '@lovett/ui/src/v2/theme.css'
6
- *
7
- * v2 is additive. Every token it defines is namespaced `--lv-*` and
8
- * every class `.lv-*`, so `theme.css` can load alongside v1's
9
- * `tokens.css` / `styles.css` without changing a single v1 pixel. A page
10
- * can migrate one section at a time.
11
- *
12
- * See ./README.md for the surface ladder, the colour formula, the
13
- * spacing scale, and when to reach for each primitive.
14
- */
15
-
16
- /* ── Tokens & scales ─────────────────────────────────────────────── */
17
- export {
18
- SPACE_STEPS,
19
- SPACE_PX,
20
- SERIES,
21
- ELEVATION_INSET,
22
- DOC_HEADER_HEIGHT,
23
- DOC_NAV_WIDTH,
24
- DOC_TWO_COLUMN_MIN_WIDTH,
25
- SCROLL_OFFSET,
26
- space,
27
- radius,
28
- elevation,
29
- duration,
30
- easing,
31
- surface,
32
- textColor,
33
- toneText,
34
- toneMark,
35
- toneSoft,
36
- alignValue,
37
- justifyValue,
38
- type SpaceStep,
39
- type RadiusStep,
40
- type TextRole,
41
- type ElevationStep,
42
- type Duration,
43
- type Easing,
44
- type SurfaceTier,
45
- type TextTone,
46
- type SemanticTone,
47
- type Tone,
48
- type AlignItems,
49
- type JustifyContent,
50
- } from './tokens'
51
-
52
- /* ── Layout primitives ───────────────────────────────────────────── */
53
- export {
54
- Stack,
55
- Inline,
56
- Grid,
57
- Section,
58
- Panel,
59
- Toolbar,
60
- type StackProps,
61
- type InlineProps,
62
- type GridProps,
63
- type SectionProps,
64
- type PanelProps,
65
- type PanelTone,
66
- type ToolbarProps,
67
- } from './layout'
68
-
69
- /* ── Action ──────────────────────────────────────────────────────── */
70
- export {
71
- Action,
72
- type ActionProps,
73
- type ActionVariant,
74
- type ActionSize,
75
- } from './action'
76
-
77
- /* ── Document / brief primitives ─────────────────────────────────── */
78
- export {
79
- DocumentShell,
80
- type DocumentShellProps,
81
- type DocumentNavItem,
82
- } from './document-shell'
83
-
84
- export {
85
- DocumentSection,
86
- Prose,
87
- type DocumentSectionProps,
88
- } from './document-section'
89
-
90
- export {
91
- DefinitionList,
92
- FieldRow,
93
- type DefinitionListProps,
94
- type FieldRowProps,
95
- } from './field-row'
96
-
97
- export { StatTile, type StatTileProps, type DeltaTone } from './stat-tile'
98
-
99
- export { Callout, type CalloutProps, type CalloutTone } from './callout'
100
-
101
- export {
102
- StatusPill,
103
- type StatusPillProps,
104
- type StatusTone,
105
- type MarkShape,
106
- } from './status-pill'
107
-
108
- export {
109
- ProgressTrack,
110
- type ProgressTrackProps,
111
- type ProgressTone,
112
- } from './progress-track'
113
-
114
- export {
115
- Timeline,
116
- type TimelineProps,
117
- type TimelineItem,
118
- type TimelineState,
119
- } from './timeline'
120
-
121
- /* ── States ──────────────────────────────────────────────────────── */
122
- export {
123
- EmptyState,
124
- ErrorState,
125
- LoadingSkeleton,
126
- type EmptyStateProps,
127
- type ErrorStateProps,
128
- type LoadingSkeletonProps,
129
- type SkeletonVariant,
130
- } from './states'
131
-
132
- /* ── Icons ───────────────────────────────────────────────────────── */
133
- export {
134
- IconInfo,
135
- IconSuccess,
136
- IconWarning,
137
- IconDanger,
138
- IconEmpty,
139
- IconRetry,
140
- IconCheck,
141
- IconChevronRight,
142
- IconArrowUp,
143
- IconArrowDown,
144
- IconFlat,
145
- IconDot,
146
- type IconProps,
147
- } from './icons'