@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.
- package/dist/index.d.ts +2659 -0
- package/dist/index.js +14451 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +636 -0
- package/dist/tokens.css +700 -0
- package/package.json +44 -17
- package/src/__tests__/button.test.tsx +137 -0
- package/src/__tests__/card.test.tsx +103 -0
- package/src/__tests__/dead-render.test.tsx +117 -0
- package/src/__tests__/input.test.tsx +134 -0
- package/src/__tests__/modal.test.tsx +154 -0
- package/src/__tests__/page-shell.test.tsx +128 -0
- package/src/__tests__/setup.ts +43 -0
- package/src/__tests__/token-shape.test.ts +193 -0
- package/src/allocation-sparkbar.tsx +90 -0
- package/src/card.tsx +1 -1
- package/src/collapsible-card.tsx +85 -0
- package/src/data-grid/table-body.tsx +8 -1
- package/src/dropdown-menu.tsx +1 -1
- package/src/floating-status-bar.tsx +112 -0
- package/src/folder-tree-picker.tsx +5 -6
- package/src/frame-stack.tsx +27 -10
- package/src/hero-form-card.tsx +2 -2
- package/src/icons/brand.tsx +187 -0
- package/src/index.ts +43 -0
- package/src/lib/clipboard.ts +14 -0
- package/src/lib/color.ts +111 -0
- package/src/meta-cell.tsx +52 -0
- package/src/meta-previews/MetaFeedCarousel.tsx +1 -1
- package/src/meta-previews/MetaFeedPreview.tsx +1 -1
- package/src/microsoft-logo.tsx +33 -0
- package/src/modal.tsx +77 -6
- package/src/pill-button.tsx +23 -5
- package/src/profile-section.tsx +40 -9
- package/src/sortable-table.tsx +5 -1
- package/src/styles.css +74 -0
- package/src/tabs.tsx +4 -0
- package/src/tag-chip-input.tsx +1 -1
- package/src/theme-v2.css +466 -0
- package/src/token-badge.tsx +92 -0
- package/src/tokens.css +181 -60
- package/src/v2/README.md +208 -0
- package/src/v2/__demo__/showcase.tsx +1045 -0
- package/src/v2/action.tsx +91 -0
- package/src/v2/callout.tsx +76 -0
- package/src/v2/document-section.tsx +82 -0
- package/src/v2/document-shell.tsx +0 -0
- package/src/v2/field-row.tsx +113 -0
- package/src/v2/icons.tsx +165 -0
- package/src/v2/index.ts +147 -0
- package/src/v2/layout.tsx +293 -0
- package/src/v2/progress-track.tsx +89 -0
- package/src/v2/stat-tile.tsx +129 -0
- package/src/v2/states.tsx +271 -0
- package/src/v2/status-pill.tsx +74 -0
- package/src/v2/theme.css +1861 -0
- package/src/v2/timeline.tsx +81 -0
- package/src/v2/tokens.ts +228 -0
- package/src/value-chip.tsx +76 -0
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
|
@@ -0,0 +1,113 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
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'
|