@lovett/ui 0.0.9 → 0.0.11
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 +823 -135
- package/dist/index.js +2048 -358
- package/dist/index.js.map +1 -1
- package/dist/styles.css +44 -2
- package/dist/theme-v2.css +228 -0
- package/dist/tokens.css +123 -8
- package/package.json +1 -1
- package/src/__tests__/anchor.test.tsx +422 -0
- package/src/__tests__/combobox.test.tsx +677 -0
- package/src/__tests__/dropdown-menu.test.tsx +418 -0
- package/src/__tests__/helpers/geometry.ts +58 -0
- package/src/__tests__/layer-stack.test.tsx +228 -0
- package/src/__tests__/modal.test.tsx +180 -6
- package/src/__tests__/popover.test.tsx +460 -0
- package/src/__tests__/select.test.tsx +543 -0
- package/src/__tests__/tooltip.test.tsx +355 -0
- package/src/calculator-shell-v2.tsx +19 -39
- package/src/code-block.tsx +15 -26
- package/src/combobox.tsx +796 -0
- package/src/dropdown-menu.tsx +142 -152
- package/src/icons/brand.tsx +81 -2
- package/src/index.ts +111 -0
- package/src/lib/anchor.ts +427 -0
- package/src/lib/focus.ts +32 -0
- package/src/lib/layer-stack.ts +188 -0
- package/src/lib/refs.ts +31 -0
- package/src/metric-card.tsx +57 -22
- package/src/modal.tsx +149 -9
- package/src/page-shell.tsx +91 -2
- package/src/popover.tsx +407 -0
- package/src/segmented-pill.tsx +33 -10
- package/src/select.tsx +646 -0
- package/src/stat-row.tsx +108 -70
- package/src/styles.css +44 -2
- package/src/theme-v2.css +7 -245
- package/src/tokens.css +123 -8
- package/src/tooltip.tsx +297 -0
- package/src/react-syntax-highlighter-prism.d.ts +0 -34
- package/src/v2/README.md +0 -208
- package/src/v2/__demo__/showcase.tsx +0 -1045
- package/src/v2/action.tsx +0 -91
- package/src/v2/callout.tsx +0 -76
- package/src/v2/document-section.tsx +0 -82
- package/src/v2/document-shell.tsx +0 -0
- package/src/v2/field-row.tsx +0 -113
- package/src/v2/icons.tsx +0 -165
- package/src/v2/index.ts +0 -147
- package/src/v2/layout.tsx +0 -293
- package/src/v2/progress-track.tsx +0 -89
- package/src/v2/stat-tile.tsx +0 -129
- package/src/v2/states.tsx +0 -271
- package/src/v2/status-pill.tsx +0 -74
- package/src/v2/theme.css +0 -1861
- package/src/v2/timeline.tsx +0 -81
- package/src/v2/tokens.ts +0 -228
package/src/stat-row.tsx
CHANGED
|
@@ -19,12 +19,26 @@
|
|
|
19
19
|
* applied here). Inner panels carry a fixed `min-height` so a tile never
|
|
20
20
|
* resizes as its value's length changes (ADR-076 D5 zero-layout-shift).
|
|
21
21
|
*
|
|
22
|
+
* Responsive (ADR-145 amendment, 2026-09-04): below the `sm` breakpoint the
|
|
23
|
+
* row becomes a two-column grid and the compounding arrows hide — four
|
|
24
|
+
* tiles abreast on a phone crushed the figures to nothing. Order is
|
|
25
|
+
* preserved, so the chain still reads left-to-right, top-to-bottom.
|
|
26
|
+
*
|
|
27
|
+
* `copyable` (ADR-145 amendment, 2026-09-04): every inner panel becomes a
|
|
28
|
+
* button that copies its figure (the `copyValue` override when a step sets
|
|
29
|
+
* one, else the displayed `value`) and toasts "Copied <label>". Same
|
|
30
|
+
* affordance as ValueChip: a hover lift on the panel and the focus ring.
|
|
31
|
+
* Without it the panel stays inert, so a read-only row never advertises an
|
|
32
|
+
* interaction it doesn't have.
|
|
33
|
+
*
|
|
22
34
|
* Promoted to @lovett/ui under ADR-076 D2 — the catalog of calculator
|
|
23
35
|
* consumers clears ADR-008 D3's 2+ consumer gate.
|
|
24
36
|
*/
|
|
25
37
|
|
|
26
38
|
import { ArrowRight } from 'lucide-react'
|
|
27
|
-
import { Fragment, type ReactNode } from 'react'
|
|
39
|
+
import { Fragment, type CSSProperties, type ReactNode } from 'react'
|
|
40
|
+
import { copyText } from './lib/clipboard'
|
|
41
|
+
import { cn } from './lib/utils'
|
|
28
42
|
|
|
29
43
|
export interface StatRowStep {
|
|
30
44
|
/** Lucide glyph (~16px). Inherits the dark-frame label colour. */
|
|
@@ -36,97 +50,121 @@ export interface StatRowStep {
|
|
|
36
50
|
/** The single pivotal tile — rendered in `--accent` instead of
|
|
37
51
|
* `--brand-ink`. At most one step should set this. */
|
|
38
52
|
pivotal?: boolean
|
|
53
|
+
/** What a copyable tile writes to the clipboard. Defaults to `value`;
|
|
54
|
+
* set it when the display is rounded (e.g. "833.3K") and the exact
|
|
55
|
+
* figure is what the user wants. */
|
|
56
|
+
copyValue?: string
|
|
39
57
|
}
|
|
40
58
|
|
|
41
59
|
export interface StatRowProps {
|
|
42
60
|
steps: StatRowStep[]
|
|
61
|
+
/** Make every tile's figure a copy-to-clipboard button. */
|
|
62
|
+
copyable?: boolean
|
|
43
63
|
className?: string
|
|
44
64
|
}
|
|
45
65
|
|
|
46
|
-
export function StatRow({ steps, className }: StatRowProps) {
|
|
66
|
+
export function StatRow({ steps, copyable = false, className }: StatRowProps) {
|
|
47
67
|
return (
|
|
48
|
-
<div
|
|
49
|
-
{
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
data-pivotal={s.pivotal ? 'true' : undefined}
|
|
68
|
+
<div
|
|
69
|
+
className={cn('grid grid-cols-2 gap-2 sm:flex sm:items-stretch sm:gap-0', className)}
|
|
70
|
+
>
|
|
71
|
+
{steps.map((s, i) => {
|
|
72
|
+
const panelStyle: CSSProperties = {
|
|
73
|
+
background: 'rgb(var(--bg-card))',
|
|
74
|
+
borderRadius: 10,
|
|
75
|
+
padding: '12px 10px',
|
|
76
|
+
minHeight: 46,
|
|
77
|
+
}
|
|
78
|
+
const figure = (
|
|
79
|
+
<span
|
|
80
|
+
className="font-extrabold tabular-nums"
|
|
62
81
|
style={{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
background: s.pivotal
|
|
82
|
+
fontSize: 22,
|
|
83
|
+
letterSpacing: '-0.025em',
|
|
84
|
+
lineHeight: 1,
|
|
85
|
+
color: s.pivotal
|
|
68
86
|
? 'rgb(var(--accent))'
|
|
69
|
-
: 'rgb(var(--
|
|
87
|
+
: 'rgb(var(--foreground))',
|
|
70
88
|
}}
|
|
71
89
|
>
|
|
72
|
-
{
|
|
90
|
+
{s.value}
|
|
91
|
+
</span>
|
|
92
|
+
)
|
|
93
|
+
return (
|
|
94
|
+
<Fragment key={i}>
|
|
95
|
+
{i > 0 && (
|
|
96
|
+
<div
|
|
97
|
+
className="hidden sm:flex items-center shrink-0"
|
|
98
|
+
style={{ padding: '0 2px', color: 'rgb(var(--text-muted))' }}
|
|
99
|
+
aria-hidden="true"
|
|
100
|
+
>
|
|
101
|
+
<ArrowRight size={14} strokeWidth={2.2} />
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
73
104
|
<div
|
|
74
|
-
|
|
105
|
+
data-pivotal={s.pivotal ? 'true' : undefined}
|
|
75
106
|
style={{
|
|
76
|
-
|
|
77
|
-
padding: '4px 7px 6px',
|
|
78
|
-
color: 'rgb(255 255 255 / 0.7)',
|
|
107
|
+
flex: 1,
|
|
79
108
|
minWidth: 0,
|
|
109
|
+
borderRadius: 'var(--radius-lg)',
|
|
110
|
+
padding: 5,
|
|
111
|
+
background: s.pivotal
|
|
112
|
+
? 'rgb(var(--accent))'
|
|
113
|
+
: 'rgb(var(--brand-ink))',
|
|
80
114
|
}}
|
|
81
115
|
>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
second line and unbalancing the tile row. Keep labels short. */}
|
|
86
|
-
<span
|
|
87
|
-
className="font-bold uppercase"
|
|
88
|
-
title={s.label}
|
|
116
|
+
{/* label row sits on the dark frame */}
|
|
117
|
+
<div
|
|
118
|
+
className="flex items-center"
|
|
89
119
|
style={{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
overflow: 'hidden',
|
|
94
|
-
textOverflow: 'ellipsis',
|
|
120
|
+
gap: 5,
|
|
121
|
+
padding: '4px 7px 6px',
|
|
122
|
+
color: 'rgb(255 255 255 / 0.7)',
|
|
95
123
|
minWidth: 0,
|
|
96
|
-
flex: 1,
|
|
97
|
-
}}
|
|
98
|
-
>
|
|
99
|
-
{s.label}
|
|
100
|
-
</span>
|
|
101
|
-
</div>
|
|
102
|
-
{/* recessed white inner panel — fixed min-height so the tile
|
|
103
|
-
never resizes as the value's length changes */}
|
|
104
|
-
<div
|
|
105
|
-
className="grid place-items-center text-center"
|
|
106
|
-
style={{
|
|
107
|
-
background: 'rgb(var(--bg-card))',
|
|
108
|
-
borderRadius: 10,
|
|
109
|
-
padding: '12px 10px',
|
|
110
|
-
minHeight: 46,
|
|
111
|
-
}}
|
|
112
|
-
>
|
|
113
|
-
<span
|
|
114
|
-
className="font-extrabold tabular-nums"
|
|
115
|
-
style={{
|
|
116
|
-
fontSize: 22,
|
|
117
|
-
letterSpacing: '-0.025em',
|
|
118
|
-
lineHeight: 1,
|
|
119
|
-
color: s.pivotal
|
|
120
|
-
? 'rgb(var(--accent))'
|
|
121
|
-
: 'rgb(var(--foreground))',
|
|
122
124
|
}}
|
|
123
125
|
>
|
|
124
|
-
{s.
|
|
125
|
-
|
|
126
|
+
<span className="inline-flex shrink-0">{s.icon}</span>
|
|
127
|
+
{/* Single-line cap: long labels truncate with an ellipsis
|
|
128
|
+
(full text on hover via title) instead of wrapping to a
|
|
129
|
+
second line and unbalancing the tile row. Keep labels short. */}
|
|
130
|
+
<span
|
|
131
|
+
className="font-bold uppercase"
|
|
132
|
+
title={s.label}
|
|
133
|
+
style={{
|
|
134
|
+
fontSize: '9.5px',
|
|
135
|
+
letterSpacing: '0.05em',
|
|
136
|
+
whiteSpace: 'nowrap',
|
|
137
|
+
overflow: 'hidden',
|
|
138
|
+
textOverflow: 'ellipsis',
|
|
139
|
+
minWidth: 0,
|
|
140
|
+
flex: 1,
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
{s.label}
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
{/* recessed white inner panel — fixed min-height so the tile
|
|
147
|
+
never resizes as the value's length changes */}
|
|
148
|
+
{copyable ? (
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={() => void copyText(s.copyValue ?? s.value, s.label)}
|
|
152
|
+
title={`Copy ${s.label}`}
|
|
153
|
+
aria-label={`Copy ${s.label}: ${s.value}`}
|
|
154
|
+
className="grid w-full place-items-center text-center cursor-pointer transition-[box-shadow,transform] hover:-translate-y-px hover:[box-shadow:var(--shadow-sm)] focus-visible:outline-none focus-visible:[box-shadow:var(--ring-focus)]"
|
|
155
|
+
style={{ ...panelStyle, border: 'none' }}
|
|
156
|
+
>
|
|
157
|
+
{figure}
|
|
158
|
+
</button>
|
|
159
|
+
) : (
|
|
160
|
+
<div className="grid place-items-center text-center" style={panelStyle}>
|
|
161
|
+
{figure}
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
126
164
|
</div>
|
|
127
|
-
</
|
|
128
|
-
|
|
129
|
-
)
|
|
165
|
+
</Fragment>
|
|
166
|
+
)
|
|
167
|
+
})}
|
|
130
168
|
</div>
|
|
131
169
|
)
|
|
132
170
|
}
|
package/src/styles.css
CHANGED
|
@@ -31,10 +31,24 @@
|
|
|
31
31
|
*
|
|
32
32
|
* Out of scope for Phase 1: .empty, .skel, .field*, .pg-header, .search-bar,
|
|
33
33
|
* .breadcrumbs, .topbar, .checkbox, .radio, .switch, .reasoning-pulse,
|
|
34
|
-
* .chat-md-list, .selection-reply-btn
|
|
35
|
-
*
|
|
34
|
+
* .chat-md-list, .selection-reply-btn. Those land when their consumers
|
|
35
|
+
* (Field, EmptyState, PageHeader, etc.) arrive in later phases.
|
|
36
|
+
* `::selection` landed 2026-09-04 (ADR-145 amendment) — see TEXT SELECTION.
|
|
36
37
|
*/
|
|
37
38
|
|
|
39
|
+
/* ---- TEXT SELECTION ---- */
|
|
40
|
+
/**
|
|
41
|
+
* Explicit, token-backed highlight. Without this, the only `::selection`
|
|
42
|
+
* rule in the bundle came from `@lovett/shell` (`background:
|
|
43
|
+
* var(--selection-bg)`), whose variable is scoped to `.cs-frame` — so at
|
|
44
|
+
* the root it resolved to nothing and highlighted text was invisible.
|
|
45
|
+
* `--selection-bg` is now a public token in tokens.css (light + dark).
|
|
46
|
+
*/
|
|
47
|
+
::selection {
|
|
48
|
+
background: var(--selection-bg);
|
|
49
|
+
color: inherit;
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
/* ---- PANE MASK ---- */
|
|
39
53
|
/**
|
|
40
54
|
* Opaque fill matching the MainLayout content pane, for a sticky element that
|
|
@@ -131,6 +145,34 @@
|
|
|
131
145
|
box-shadow: var(--ring-focus);
|
|
132
146
|
}
|
|
133
147
|
|
|
148
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
149
|
+
* BASELINE FOCUS INDICATOR
|
|
150
|
+
*
|
|
151
|
+
* Until this existed, only elements that opted in — primitives via .btn /
|
|
152
|
+
* .input-shell, and hand-written `focus-visible:` classes — showed OUR focus
|
|
153
|
+
* ring. Everything else fell through to the browser's `outline: auto`, which
|
|
154
|
+
* on macOS paints in the USER'S SYSTEM ACCENT COLOUR. So a keyboard user with
|
|
155
|
+
* an amber system accent saw an amber ring on most controls and our red ring
|
|
156
|
+
* on a few, in the same tab sequence. Measured in the Generate lens: 115
|
|
157
|
+
* buttons, 35 with a ring.
|
|
158
|
+
*
|
|
159
|
+
* `:where()` gives this ZERO specificity, so it is a floor, not an override —
|
|
160
|
+
* .btn, .input-shell, and any component or utility class still win.
|
|
161
|
+
* ───────────────────────────────────────────────────────────────────────── */
|
|
162
|
+
@layer base {
|
|
163
|
+
:where(a[href], button, input, select, textarea, summary, [tabindex]:not([tabindex^='-'])):focus-visible {
|
|
164
|
+
/* OUTLINE, not box-shadow, deliberately. Tailwind's shadow/ring utilities
|
|
165
|
+
compose onto `box-shadow`, so a zero-specificity box-shadow baseline loses
|
|
166
|
+
to any element carrying one (e.g. a `ring-1` selected state) and silently
|
|
167
|
+
renders nothing. `outline` is untouched by that chain, follows
|
|
168
|
+
border-radius in every modern browser, and matches --ring-focus's geometry.
|
|
169
|
+
Elements with their own ring set `outline: none` alongside it, so they
|
|
170
|
+
override this cleanly rather than double-drawing. */
|
|
171
|
+
outline: 2px solid rgb(var(--accent));
|
|
172
|
+
outline-offset: 0;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
134
176
|
.btn-primary {
|
|
135
177
|
background: rgb(var(--accent));
|
|
136
178
|
color: white;
|
package/src/theme-v2.css
CHANGED
|
@@ -1,46 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* @lovett/ui — v2 theme, SCOPED.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* SUPERSEDED FOR NEW WORK by `src/v2/theme.css`.
|
|
6
|
-
*
|
|
7
|
-
* That file is the OKLCH design layer: a namespaced `--lv-*` token set,
|
|
8
|
-
* an explicit surface ladder, elevation as shadow + inner highlight, and
|
|
9
|
-
* both appearances driven by `prefers-color-scheme` AND `data-theme`.
|
|
10
|
-
* New surfaces should import it and use `@lovett/ui/v2`.
|
|
11
|
-
*
|
|
12
|
-
* ── THAT DIRECTIVE IS NO LONGER TRUE (2026-08-20) ─────────────────────────
|
|
13
|
-
* `@lovett/ui/v2` is NOT EXPORTED any more. The `./v2`, `./v2/theme.css` and
|
|
14
|
-
* `./v2/__demo__/showcase` entries were removed from `package.json` under
|
|
15
|
-
* ADR-CE-0027 D6 step 5, on the owner's decision, after a search that reaches
|
|
16
|
-
* gitignored trees confirmed **zero consumers family-wide**. It cleared four
|
|
17
|
-
* name collisions (`EmptyState`, `EmptyStateProps`, `Panel`, `Toolbar`).
|
|
18
|
-
*
|
|
19
|
-
* NO SOURCE WAS DELETED. `src/v2/` is intact and the removal is three lines of
|
|
20
|
-
* `exports` to restore, which is why it was done this way rather than by
|
|
21
|
-
* deleting components.
|
|
22
|
-
*
|
|
23
|
-
* THIS FILE STAYS AND IS STILL LIVE — two ignite worktrees import it as
|
|
24
|
-
* `@lovett/ui/theme-v2.css` in their `index.css`. Do not confuse it with
|
|
25
|
-
* `src/v2/theme.css`, which is a different, larger file and is the one that
|
|
26
|
-
* went unexported.
|
|
27
|
-
*
|
|
28
|
-
* Consequence to know about: the v2 BRIDGE below (section 3) maps `--lv-*` so
|
|
29
|
-
* v2 primitives render in a `data-ds="v2"` subtree. With no public `./v2`
|
|
30
|
-
* entry, nothing can import those primitives, so the bridge is inert. It is
|
|
31
|
-
* left in place because it is harmless, because removing it is the source
|
|
32
|
-
* surgery this change deliberately avoided, and because it becomes live again
|
|
33
|
-
* the moment the exports are restored.
|
|
34
|
-
*
|
|
35
|
-
* This file stays because live consumers depend on it. Everything ABOVE
|
|
36
|
-
* the "APPENDED" marker near the bottom is untouched and still generated
|
|
37
|
-
* — every existing declaration resolves to exactly the value it always
|
|
38
|
-
* did. The appended region is hand-authored and additive: it bridges
|
|
39
|
-
* this scope onto the v2 token names and adds the automatic-dark support
|
|
40
|
-
* this file never had. If you re-run the generator, re-apply that region.
|
|
41
|
-
* ─────────────────────────────────────────────────────────────────────
|
|
42
|
-
*
|
|
43
|
-
* GENERATED (region above the marker). Regenerate with:
|
|
4
|
+
* GENERATED. Regenerate with:
|
|
44
5
|
* python3 scripts/figma/emit-theme.py <corpus> --out packages/ui/src/theme-v2.css
|
|
45
6
|
*
|
|
46
7
|
* Opt in by putting `data-ds="v2"` on any element. Everything inside it
|
|
@@ -193,8 +154,11 @@
|
|
|
193
154
|
--card-shadow-hover: 0 0 0 1px oklch(0 0 0 / 0.08), 0 2px 4px -1px oklch(0 0 0 / 0.08), 0 6px 14px -2px oklch(0 0 0 / 0.08), inset 0 1px 0 0 oklch(1 0 0 / 0.9);
|
|
194
155
|
--inset-highlight: 255 255 255 / 0.9;
|
|
195
156
|
--modal-overlay: 0 0 0 / 0.32;
|
|
196
|
-
|
|
197
|
-
|
|
157
|
+
/* Solid, matching the base theme — a 3px translucent halo measures ~1.4:1
|
|
158
|
+
against its surface, under the 3:1 WCAG 2.2 SC 2.4.11 asks of a focus
|
|
159
|
+
indicator. See the note on --ring-focus in tokens.css. */
|
|
160
|
+
--ring-focus: 0 0 0 2px rgb(var(--accent));
|
|
161
|
+
--ring-error: 0 0 0 2px rgb(var(--destructive));
|
|
198
162
|
}
|
|
199
163
|
|
|
200
164
|
/*
|
|
@@ -260,207 +224,5 @@
|
|
|
260
224
|
--card-shadow-hover: 0 0 0 1px oklch(1 0 0 / 0.14);
|
|
261
225
|
--inset-highlight: 255 255 255 / 0.05;
|
|
262
226
|
--modal-overlay: 0 0 0 / 0.7;
|
|
263
|
-
--ring-focus: 0 0 0
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/* ══════════════════════════════════════════════════════════════════════
|
|
267
|
-
═══════════════════════════ APPENDED ═══════════════════════════════
|
|
268
|
-
Hand-authored, additive. Nothing above this line was changed.
|
|
269
|
-
Three things this region adds:
|
|
270
|
-
|
|
271
|
-
1. AUTOMATIC DARK. The generated file only goes dark on an explicit
|
|
272
|
-
`data-theme="dark"`. A surface that wants to follow the OS opts
|
|
273
|
-
in with `data-theme="auto"`; the block below is a verbatim copy
|
|
274
|
-
of the explicit-dark map, because a media query and an attribute
|
|
275
|
-
selector cannot share a rule. Keep the two in sync.
|
|
276
|
-
|
|
277
|
-
2. THE SURFACE LADDER, as first-class names. The generated file
|
|
278
|
-
encodes the ladder implicitly across a dozen aliases
|
|
279
|
-
(--page-bg / --bg-surface / --bg-card / --tray-bg ...). These
|
|
280
|
-
five names say what the tier IS, which is what a component
|
|
281
|
-
should be reading.
|
|
282
|
-
|
|
283
|
-
3. A BRIDGE to the v2 `--lv-*` tokens, so `@lovett/ui/v2`
|
|
284
|
-
primitives render correctly inside an existing `data-ds="v2"`
|
|
285
|
-
subtree without also loading `src/v2/theme.css`. Migration can
|
|
286
|
-
then happen one component at a time.
|
|
287
|
-
══════════════════════════════════════════════════════════════════════ */
|
|
288
|
-
|
|
289
|
-
/* ── 1. Automatic dark (opt-in via data-theme="auto") ─────────────── */
|
|
290
|
-
|
|
291
|
-
@media (prefers-color-scheme: dark) {
|
|
292
|
-
[data-ds='v2'][data-theme='auto'] {
|
|
293
|
-
color-scheme: dark;
|
|
294
|
-
|
|
295
|
-
--page-bg: 11 16 28;
|
|
296
|
-
--background: 11 16 28;
|
|
297
|
-
--main-content-bg: 11 16 28;
|
|
298
|
-
--bg-surface: 27 35 51;
|
|
299
|
-
--surface-frame: 27 35 51;
|
|
300
|
-
--card-frame-bg: 27 35 51;
|
|
301
|
-
--bg-card: 48 58 80;
|
|
302
|
-
--card: 48 58 80;
|
|
303
|
-
--tray-bg: 48 58 80;
|
|
304
|
-
--bg-elevated: 70 82 109;
|
|
305
|
-
--surface-raised: 70 82 109;
|
|
306
|
-
--popover: 70 82 109;
|
|
307
|
-
--bg-input: 27 35 51;
|
|
308
|
-
--bg-card-hover: 70 82 109;
|
|
309
|
-
--bg-input-hover: 48 58 80;
|
|
310
|
-
--muted: 48 58 80;
|
|
311
|
-
--canvas-placement-bg: 27 35 51;
|
|
312
|
-
--foreground: 250 251 252;
|
|
313
|
-
--card-foreground: 250 251 252;
|
|
314
|
-
--popover-foreground: 250 251 252;
|
|
315
|
-
--text-secondary: 192 193 195;
|
|
316
|
-
--muted-foreground: 192 193 195;
|
|
317
|
-
--text-tertiary: 162 163 165;
|
|
318
|
-
--text-muted: 133 134 136;
|
|
319
|
-
--secondary: 48 58 80;
|
|
320
|
-
--secondary-foreground: 250 251 252;
|
|
321
|
-
--border: 255 255 255 / 0.08;
|
|
322
|
-
--border-strong: 255 255 255 / 0.14;
|
|
323
|
-
--border-card: 255 255 255 / 0.08;
|
|
324
|
-
--border-input: 255 255 255 / 0.14;
|
|
325
|
-
--main-content-border: 255 255 255 / 0.08;
|
|
326
|
-
--accent: 228 70 58;
|
|
327
|
-
--accent-bright: 251 114 99;
|
|
328
|
-
--accent-hover: 251 114 99;
|
|
329
|
-
--primary: 228 70 58;
|
|
330
|
-
--primary-hover: 251 114 99;
|
|
331
|
-
--success: 80 188 126;
|
|
332
|
-
--warning: 190 160 64;
|
|
333
|
-
--destructive: 218 145 55;
|
|
334
|
-
--info: 181 134 254;
|
|
335
|
-
--success-bg: 6 160 93 / 0.16;
|
|
336
|
-
--warning-bg: 161 131 18 / 0.16;
|
|
337
|
-
--destructive-bg: 187 116 2 / 0.16;
|
|
338
|
-
--info-bg: 155 97 235 / 0.16;
|
|
339
|
-
--shadow-sm: 0 0 0 1px oklch(1 0 0 / 0.08);
|
|
340
|
-
--shadow-md: 0 0 0 1px oklch(1 0 0 / 0.08), 0 4px 12px oklch(0 0 0 / 0.4);
|
|
341
|
-
--shadow-lg: 0 0 0 1px oklch(1 0 0 / 0.08), 0 8px 24px oklch(0 0 0 / 0.5);
|
|
342
|
-
--shadow-xl: 0 0 0 1px oklch(1 0 0 / 0.1), 0 16px 40px oklch(0 0 0 / 0.55);
|
|
343
|
-
--shadow-2xl: 0 0 0 1px oklch(1 0 0 / 0.1), 0 24px 60px oklch(0 0 0 / 0.6);
|
|
344
|
-
--card-shadow-rest: 0 0 0 1px oklch(1 0 0 / 0.08);
|
|
345
|
-
--card-shadow-hover: 0 0 0 1px oklch(1 0 0 / 0.14);
|
|
346
|
-
--inset-highlight: 255 255 255 / 0.05;
|
|
347
|
-
--modal-overlay: 0 0 0 / 0.7;
|
|
348
|
-
--ring-focus: 0 0 0 3px rgb(228 70 58 / 0.3);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/* `data-theme` must win in BOTH directions: forced light under a dark OS
|
|
353
|
-
is as much a requirement as forced dark under a light one. The two
|
|
354
|
-
explicit values are already unconditional rules, so they beat the
|
|
355
|
-
media query above by source order; this only declares the native
|
|
356
|
-
`color-scheme` so form controls and scrollbars agree. */
|
|
357
|
-
[data-ds='v2'][data-theme='light'] {
|
|
358
|
-
color-scheme: light;
|
|
359
|
-
}
|
|
360
|
-
[data-ds='v2'][data-theme='dark'] {
|
|
361
|
-
color-scheme: dark;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/* ── 2. The surface ladder, named ─────────────────────────────────── */
|
|
365
|
-
/* page → shell → section → card, each one notch brighter; `inset` goes
|
|
366
|
-
the other way, because a recessed panel inside a card is the system's
|
|
367
|
-
strongest single move and it only reads as recessed if it is darker.
|
|
368
|
-
Aliases, not new values — these resolve to what the tiers already are. */
|
|
369
|
-
|
|
370
|
-
[data-ds='v2'] {
|
|
371
|
-
--surface-page: rgb(var(--page-bg));
|
|
372
|
-
--surface-shell: rgb(var(--bg-surface));
|
|
373
|
-
--surface-section: rgb(var(--muted));
|
|
374
|
-
--surface-card: rgb(var(--bg-card));
|
|
375
|
-
--surface-inset: rgb(var(--canvas-placement-bg));
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
@media (prefers-color-scheme: dark) {
|
|
379
|
-
[data-ds='v2'][data-theme='auto'] {
|
|
380
|
-
--surface-section: rgb(27 35 51);
|
|
381
|
-
--surface-inset: rgb(27 35 51);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
[data-ds='v2'][data-theme='dark'] {
|
|
386
|
-
--surface-section: rgb(27 35 51);
|
|
387
|
-
--surface-inset: rgb(27 35 51);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/* ── 3. Bridge to the v2 `--lv-*` tokens ──────────────────────────── */
|
|
391
|
-
/* Lets `@lovett/ui/v2` primitives render inside this scope. Only the
|
|
392
|
-
tokens v2 components actually read are mapped; the scales (space,
|
|
393
|
-
radius, type, motion) are appearance-independent and are defined
|
|
394
|
-
unconditionally in `src/v2/theme.css`, which must still be imported
|
|
395
|
-
for those. This block covers colour and depth only — the parts that
|
|
396
|
-
have to agree with whichever theme the subtree is on. */
|
|
397
|
-
|
|
398
|
-
[data-ds='v2'] {
|
|
399
|
-
--lv-surface-page: var(--surface-page);
|
|
400
|
-
--lv-surface-shell: var(--surface-shell);
|
|
401
|
-
--lv-surface-section: var(--surface-section);
|
|
402
|
-
--lv-surface-card: var(--surface-card);
|
|
403
|
-
--lv-surface-inset: var(--surface-inset);
|
|
404
|
-
--lv-surface-raised: rgb(var(--surface-raised));
|
|
405
|
-
--lv-surface-overlay: rgb(var(--modal-overlay));
|
|
406
|
-
--lv-surface-hover: rgb(var(--bg-card-hover));
|
|
407
|
-
--lv-surface-active: rgb(var(--border-strong));
|
|
408
|
-
--lv-surface-selected: rgb(var(--accent-muted));
|
|
409
|
-
|
|
410
|
-
--lv-text-primary: rgb(var(--foreground));
|
|
411
|
-
--lv-text-secondary: rgb(var(--text-secondary));
|
|
412
|
-
--lv-text-tertiary: rgb(var(--text-tertiary));
|
|
413
|
-
--lv-text-faint: rgb(var(--text-muted));
|
|
414
|
-
--lv-text-disabled: rgb(var(--text-muted));
|
|
415
|
-
--lv-text-on-accent: rgb(var(--primary-foreground));
|
|
416
|
-
|
|
417
|
-
--lv-hairline: rgb(var(--border));
|
|
418
|
-
--lv-hairline-strong: rgb(var(--border-strong));
|
|
419
|
-
--lv-border-field: rgb(var(--border-input));
|
|
420
|
-
|
|
421
|
-
--lv-accent: rgb(var(--accent));
|
|
422
|
-
--lv-accent-hover: rgb(var(--accent-hover));
|
|
423
|
-
--lv-accent-active: rgb(var(--accent-hover));
|
|
424
|
-
--lv-accent-text: rgb(var(--accent));
|
|
425
|
-
--lv-accent-mark: rgb(var(--accent-bright));
|
|
426
|
-
--lv-accent-soft: rgb(var(--accent-muted));
|
|
427
|
-
--lv-accent-rule: rgb(var(--accent-glow));
|
|
428
|
-
--lv-focus: rgb(var(--ring));
|
|
429
|
-
|
|
430
|
-
/* The generated palette's semantic reassignment carries straight over:
|
|
431
|
-
success green, warning gold, destructive amber, info violet. v2's
|
|
432
|
-
own danger hue (48°) was chosen for the same reason this file moved
|
|
433
|
-
destructive off red — the brand is red, and two reds on one page
|
|
434
|
-
cannot be told apart. */
|
|
435
|
-
--lv-success-text: rgb(var(--success));
|
|
436
|
-
--lv-success-mark: rgb(var(--success));
|
|
437
|
-
--lv-success-soft: rgb(var(--success-bg));
|
|
438
|
-
--lv-success-rule: rgb(var(--success));
|
|
439
|
-
|
|
440
|
-
--lv-warning-text: rgb(var(--warning));
|
|
441
|
-
--lv-warning-mark: rgb(var(--warning));
|
|
442
|
-
--lv-warning-soft: rgb(var(--warning-bg));
|
|
443
|
-
--lv-warning-rule: rgb(var(--warning));
|
|
444
|
-
|
|
445
|
-
--lv-danger-text: rgb(var(--destructive));
|
|
446
|
-
--lv-danger-mark: rgb(var(--destructive));
|
|
447
|
-
--lv-danger-soft: rgb(var(--destructive-bg));
|
|
448
|
-
--lv-danger-rule: rgb(var(--destructive));
|
|
449
|
-
|
|
450
|
-
--lv-info-text: rgb(var(--info));
|
|
451
|
-
--lv-info-mark: rgb(var(--info));
|
|
452
|
-
--lv-info-soft: rgb(var(--info-bg));
|
|
453
|
-
--lv-info-rule: rgb(var(--info));
|
|
454
|
-
|
|
455
|
-
--lv-highlight: inset 0 1px 0 0 rgb(var(--inset-highlight));
|
|
456
|
-
--lv-elev-0: none;
|
|
457
|
-
--lv-elev-1: var(--shadow-sm);
|
|
458
|
-
--lv-elev-2: var(--shadow-md);
|
|
459
|
-
--lv-elev-3: var(--shadow-lg);
|
|
460
|
-
--lv-elev-4: var(--shadow-xl);
|
|
461
|
-
--lv-elev-inset: inset 0 1px 2px 0 oklch(0 0 0 / 0.07),
|
|
462
|
-
inset 0 0 0 1px rgb(var(--border));
|
|
463
|
-
|
|
464
|
-
--lv-skeleton-base: rgb(var(--border-strong));
|
|
465
|
-
--lv-skeleton-sheen: rgb(var(--border));
|
|
227
|
+
--ring-focus: 0 0 0 2px rgb(var(--accent));
|
|
466
228
|
}
|