@tristan2828/ui-foundation 3.1.1 → 3.2.0
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/conventions/AGENTS.md +7 -0
- package/conventions/docs/cell-patterns.md +22 -1
- package/conventions/docs/design-language.md +22 -10
- package/dist/components/app/app-shell.js +15 -3
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/badge.js +4 -1
- package/dist/components/ui/sidebar.js +8 -3
- package/package.json +1 -1
- package/styles/index.css +3 -0
- package/styles/theme.css +12 -2
package/conventions/AGENTS.md
CHANGED
|
@@ -51,6 +51,13 @@ is right. Do not disable, skip, or work around it.
|
|
|
51
51
|
- NEVER import `@tristan2828/ui-foundation/gateway` outside
|
|
52
52
|
`src/api/gateway/`. Components and hooks call a gateway module through
|
|
53
53
|
TanStack Query. (Enforced: no-restricted-imports.)
|
|
54
|
+
- NEVER add a database table or column without a comment. The migration
|
|
55
|
+
that creates it also runs `COMMENT ON TABLE` / `COMMENT ON COLUMN`,
|
|
56
|
+
saying what it holds in words someone with only database access can use:
|
|
57
|
+
units, what null means, which table an id points at. A second agent with
|
|
58
|
+
credentials and no repo reads the schema alone. Applies to an app with a
|
|
59
|
+
Postgres backend. (Enforced: `backend/scripts/check_db_comments.py`, run
|
|
60
|
+
by `scripts/check-backend-postgres.sh` after `alembic upgrade head`.)
|
|
54
61
|
- NEVER let a backend-shaped response reach a component. Paginated data is
|
|
55
62
|
`Page<T>`. Failures are `AppError`. Queries are `QuerySpec`. The gateway
|
|
56
63
|
translates; nothing above it knows the wire format. (Enforced: gateway
|
|
@@ -162,7 +162,28 @@ When the useful question is "how soon?" rather than "which day?".
|
|
|
162
162
|
rebuilt as UTC midnight** (`Date.UTC(y, m, d)`). Subtracting `Date.now()`
|
|
163
163
|
shifts the answer by a day for anyone west of UTC.
|
|
164
164
|
- Relative text **goes stale**, so a fixture pinning a date drifts. Freeze
|
|
165
|
-
the clock (Playwright's `page.clock`) in any test asserting one
|
|
165
|
+
the clock (Playwright's `page.clock`) in any test asserting one, and
|
|
166
|
+
assert one past and one future value:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// West of UTC, where subtracting Date.now() would be off by a day.
|
|
170
|
+
test.use({ timezoneId: 'America/Los_Angeles' })
|
|
171
|
+
|
|
172
|
+
test('release dates read relative to today', async ({ page }) => {
|
|
173
|
+
// Before goto, so the first render already sees it. Noon UTC is the same
|
|
174
|
+
// calendar day in every timezone from UTC-11 to UTC+11.
|
|
175
|
+
await page.clock.setFixedTime(new Date('2026-06-01T12:00:00Z'))
|
|
176
|
+
await page.goto('/games')
|
|
177
|
+
// Mock fixtures: one released 2026-04-01, one due 2026-06-22.
|
|
178
|
+
await expect(page.getByRole('cell', { name: '2 months ago' })).toBeVisible()
|
|
179
|
+
await expect(page.getByRole('cell', { name: 'in 3 weeks' })).toBeVisible()
|
|
180
|
+
})
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`setFixedTime` pins `Date.now()` and `new Date()` and leaves the real
|
|
184
|
+
timers alone, so debounces and TanStack Query behave as usual. Reach for
|
|
185
|
+
`page.clock.install()` only when a test has to move time forward.
|
|
186
|
+
|
|
166
187
|
- `title` is mouse-only. If the exact date matters, use pattern 4 instead.
|
|
167
188
|
- **Cost:** "2 years ago" is vague, and the column doesn't look sorted even
|
|
168
189
|
though server-side sorting still works.
|
|
@@ -8,16 +8,24 @@ composites don't grow that way; they are built against a real app's screen.
|
|
|
8
8
|
|
|
9
9
|
## Semantic tones
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
is not a tone — it is the absence of one, and most values
|
|
11
|
+
Four tones carry meaning: **success**, **warning**, **destructive** and
|
|
12
|
+
**info**. Grey is not a tone — it is the absence of one, and most values
|
|
13
|
+
should stay grey.
|
|
14
|
+
|
|
15
|
+
The first three say good or bad. **Info** says neither: *notice this*,
|
|
16
|
+
without a verdict. It is for a state that is worth seeing but isn't good
|
|
17
|
+
news or bad news: in progress, new, scheduled, beta. If the honest answer
|
|
18
|
+
to "is this good?" is "neither, but it matters right now", it's info. If
|
|
19
|
+
the answer is "neither, and it doesn't", it's grey: info is still a
|
|
20
|
+
colour, and the "about a third" limit below counts it.
|
|
13
21
|
|
|
14
22
|
Each tone has two kinds of token, and picking the wrong one is the single
|
|
15
23
|
most common way to ship an unreadable label:
|
|
16
24
|
|
|
17
25
|
| Token | For | Why it can't be the other one |
|
|
18
26
|
|---|---|---|
|
|
19
|
-
| `--success` / `--warning` / `--destructive` (+ `-foreground`) | a **solid fill**, always with white text on it | chosen to contrast with *white*, so on the page background it's too light in light mode |
|
|
20
|
-
| `--success-text` / `--warning-text` / `--destructive-text` | the tone as **text, a border or an icon** on the page background | chosen to contrast with *the page*, so as a fill under white text it's needlessly dark |
|
|
27
|
+
| `--success` / `--warning` / `--destructive` / `--info` (+ `-foreground`) | a **solid fill**, always with white text on it | chosen to contrast with *white*, so on the page background it's too light in light mode |
|
|
28
|
+
| `--success-text` / `--warning-text` / `--destructive-text` / `--info-text` | the tone as **text, a border or an icon** on the page background | chosen to contrast with *the page*, so as a fill under white text it's needlessly dark |
|
|
21
29
|
|
|
22
30
|
Only the `*-text` tokens are remapped in `.dark`. A solid fill keeps one
|
|
23
31
|
shade in both themes, because it always carries white text and the page
|
|
@@ -25,20 +33,22 @@ behind it never enters the calculation.
|
|
|
25
33
|
|
|
26
34
|
Measured ratios (WCAG 2.x, sRGB) are recorded next to each primitive in
|
|
27
35
|
the package's `styles/theme.css`, and its `e2e/storybook-visual.spec.ts` re-checks all
|
|
28
|
-
|
|
36
|
+
twelve tone/style combinations with axe against the rendered DOM in both
|
|
29
37
|
themes. **A new tone or style is not done until it appears in that story.**
|
|
30
38
|
|
|
31
39
|
## The three badge styles
|
|
32
40
|
|
|
33
41
|
`Badge` ships each tone in three styles. They are not interchangeable:
|
|
34
42
|
|
|
35
|
-
- **Outline** — `outline-success`, `outline-warning`, `outline-destructive
|
|
43
|
+
- **Outline** — `outline-success`, `outline-warning`, `outline-destructive`,
|
|
44
|
+
`outline-info`.
|
|
36
45
|
The default choice, and the one proven across real columns. Quiet enough
|
|
37
46
|
to repeat down 25 rows without the column becoming a wall of color.
|
|
38
|
-
- **Tinted** — `tinted-success`, `tinted-warning`, `tinted-destructive
|
|
47
|
+
- **Tinted** — `tinted-success`, `tinted-warning`, `tinted-destructive`,
|
|
48
|
+
`tinted-info`. A
|
|
39
49
|
low-alpha wash of the tone with `*-text` on top. More presence than
|
|
40
50
|
outline; use when the value is the point of the row, not a detail of it.
|
|
41
|
-
- **Solid** — `success`, `warning`, `destructive`. The loudest. Use for one
|
|
51
|
+
- **Solid** — `success`, `warning`, `destructive`, `info`. The loudest. Use for one
|
|
42
52
|
value that must be impossible to miss, rarely for a whole column: solid
|
|
43
53
|
reads as an alert, and a column of alerts reads as noise.
|
|
44
54
|
|
|
@@ -75,7 +85,7 @@ Two rules that keep this honest:
|
|
|
75
85
|
|
|
76
86
|
## Categorical colour
|
|
77
87
|
|
|
78
|
-
Tones say good or
|
|
88
|
+
Tones say good, bad or *notice this*. A value that is merely *different* from its
|
|
79
89
|
neighbours — a genre, a team, a category — needs a different mechanism, and
|
|
80
90
|
it is `--category-1` through `--category-8`.
|
|
81
91
|
|
|
@@ -98,7 +108,9 @@ Four things about them:
|
|
|
98
108
|
|
|
99
109
|
- **The hues avoid red, green and amber**, which the tones already spend. A
|
|
100
110
|
category drawn in red reads as "this one is bad" even when nothing is
|
|
101
|
-
wrong.
|
|
111
|
+
wrong. Info's blue came later, and slots 1, 6 and 8 sit near it: a table
|
|
112
|
+
that shows an info badge keeps its categories off those three, so a blue
|
|
113
|
+
genre isn't read as "notice this".
|
|
102
114
|
- **They are ordered by distinctness, not by hue angle.** An app using
|
|
103
115
|
three categories gets three obviously different colours. Past about five,
|
|
104
116
|
hue alone stops separating them — slots 3 and 7 are both yellow-greens —
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { LogOutIcon, MoonIcon, SunIcon } from 'lucide-react';
|
|
3
3
|
import { useTheme } from 'next-themes';
|
|
4
|
-
import { useState } from 'react';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
5
|
import { NavLink, Navigate, Outlet, useLocation } from 'react-router';
|
|
6
6
|
import { useAuth } from '../../auth/use-auth.js';
|
|
7
7
|
import { ErrorState } from './error-state.js';
|
|
8
8
|
import { Button } from '../ui/button.js';
|
|
9
9
|
import { Skeleton } from '../ui/skeleton.js';
|
|
10
|
-
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger, } from '../ui/sidebar.js';
|
|
10
|
+
import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger, useSidebar, } from '../ui/sidebar.js';
|
|
11
11
|
import { Toaster } from '../ui/sonner.js';
|
|
12
12
|
import { TooltipProvider } from '../ui/tooltip.js';
|
|
13
13
|
// Must match SIDEBAR_COOKIE_NAME in ../ui/sidebar.tsx, which writes the
|
|
@@ -17,6 +17,18 @@ function storedSidebarOpen(fallback) {
|
|
|
17
17
|
const match = document.cookie.match(new RegExp(`(?:^|; )${SIDEBAR_COOKIE_NAME}=([^;]*)`));
|
|
18
18
|
return match ? match[1] === 'true' : fallback;
|
|
19
19
|
}
|
|
20
|
+
// Below the mobile breakpoint the sidebar is a modal sheet, and the
|
|
21
|
+
// primitive leaves it open when one of its links navigates, covering the
|
|
22
|
+
// page the user just asked for. Keyed on location.key, so following a link
|
|
23
|
+
// to the page already showing closes it too. Covers sidebarExtra's links.
|
|
24
|
+
function CloseMobileSidebarOnNavigate() {
|
|
25
|
+
const { setOpenMobile } = useSidebar();
|
|
26
|
+
const { key } = useLocation();
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
setOpenMobile(false);
|
|
29
|
+
}, [key, setOpenMobile]);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
20
32
|
function ThemeToggle() {
|
|
21
33
|
const { resolvedTheme, setTheme } = useTheme();
|
|
22
34
|
const isDark = resolvedTheme === 'dark';
|
|
@@ -52,7 +64,7 @@ export function AppShell({ title, nav, sidebarExtra, defaultSidebarOpen = true }
|
|
|
52
64
|
const from = `${location.pathname}${location.search}${location.hash}`;
|
|
53
65
|
return _jsx(Navigate, { to: "/login", replace: true, state: isLoggingOut ? undefined : { from } });
|
|
54
66
|
}
|
|
55
|
-
return (_jsxs(TooltipProvider, { children: [_jsxs(SidebarProvider, { defaultOpen: storedSidebarOpen(defaultSidebarOpen), children: [_jsx("nav", { "aria-label": "Primary", children: _jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsx("span", { className: "px-2 text-sm font-semibold text-sidebar-foreground group-data-[collapsible=icon]:hidden", children: title }) }), _jsxs(SidebarContent, { children: [_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: "Navigation" }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: nav.map(({ to, label, icon: Icon, end }) => {
|
|
67
|
+
return (_jsxs(TooltipProvider, { children: [_jsxs(SidebarProvider, { defaultOpen: storedSidebarOpen(defaultSidebarOpen), children: [_jsx(CloseMobileSidebarOnNavigate, {}), _jsx("nav", { "aria-label": "Primary", children: _jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsx("span", { className: "px-2 text-sm font-semibold text-sidebar-foreground group-data-[collapsible=icon]:hidden", children: title }) }), _jsxs(SidebarContent, { children: [_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: "Navigation" }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: nav.map(({ to, label, icon: Icon, end }) => {
|
|
56
68
|
const isActive = end
|
|
57
69
|
? location.pathname === to
|
|
58
70
|
: location.pathname.startsWith(to);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useRender } from "@base-ui/react/use-render";
|
|
2
2
|
import { type VariantProps } from "class-variance-authority";
|
|
3
3
|
declare const badgeVariants: (props?: ({
|
|
4
|
-
variant?: "link" | "success" | "default" | "outline" | "secondary" | "ghost" | "destructive" | "warning" | "outline-success" | "outline-warning" | "outline-destructive" | "tinted-success" | "tinted-warning" | "tinted-destructive" | null | undefined;
|
|
4
|
+
variant?: "link" | "success" | "default" | "outline" | "secondary" | "ghost" | "destructive" | "warning" | "info" | "outline-success" | "outline-warning" | "outline-info" | "outline-destructive" | "tinted-success" | "tinted-warning" | "tinted-info" | "tinted-destructive" | null | undefined;
|
|
5
5
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
6
|
declare function Badge({ className, variant, render, ...props }: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
7
7
|
export { Badge, badgeVariants };
|
|
@@ -10,14 +10,16 @@ const badgeVariants = cva("group/badge inline-flex h-5 w-fit shrink-0 items-cent
|
|
|
10
10
|
destructive: "bg-destructive text-destructive-foreground focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/90",
|
|
11
11
|
success: "bg-success text-success-foreground focus-visible:ring-success/20 dark:focus-visible:ring-success/40 [a]:hover:bg-success/90",
|
|
12
12
|
warning: "bg-warning text-warning-foreground focus-visible:ring-warning/20 dark:focus-visible:ring-warning/40 [a]:hover:bg-warning/90",
|
|
13
|
+
info: "bg-info text-info-foreground focus-visible:ring-info/20 dark:focus-visible:ring-info/40 [a]:hover:bg-info/90",
|
|
13
14
|
outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
|
14
|
-
//
|
|
15
|
+
// Four tones x three styles. The *-text tokens, never the solid
|
|
15
16
|
// fill tokens: `border-destructive`/`text-destructive` would use
|
|
16
17
|
// the shade chosen to carry white text, which is too light to read
|
|
17
18
|
// against the page in light mode and too dark in dark mode. See
|
|
18
19
|
// styles/theme.css and conventions/docs/design-language.md.
|
|
19
20
|
"outline-success": "border-success-text text-success-text",
|
|
20
21
|
"outline-warning": "border-warning-text text-warning-text",
|
|
22
|
+
"outline-info": "border-info-text text-info-text",
|
|
21
23
|
"outline-destructive": "border-destructive-text text-destructive-text",
|
|
22
24
|
// The fill is the solid token at low alpha (a wash of the tone),
|
|
23
25
|
// while the text is the *-text shade — mixing the two is the point.
|
|
@@ -25,6 +27,7 @@ const badgeVariants = cva("group/badge inline-flex h-5 w-fit shrink-0 items-cent
|
|
|
25
27
|
// against a near-black page.
|
|
26
28
|
"tinted-success": "bg-success/15 text-success-text dark:bg-success/20",
|
|
27
29
|
"tinted-warning": "bg-warning/15 text-warning-text dark:bg-warning/20",
|
|
30
|
+
"tinted-info": "bg-info/15 text-info-text dark:bg-info/20",
|
|
28
31
|
"tinted-destructive": "bg-destructive/15 text-destructive-text dark:bg-destructive/20",
|
|
29
32
|
ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
|
30
33
|
link: "text-primary underline-offset-4 hover:underline",
|
|
@@ -182,12 +182,17 @@ const sidebarMenuButtonVariants = cva("peer/menu-button group/menu-button flex w
|
|
|
182
182
|
});
|
|
183
183
|
function SidebarMenuButton({ render, isActive = false, variant = "default", size = "default", tooltip, className, ...props }) {
|
|
184
184
|
const { isMobile, state } = useSidebar();
|
|
185
|
+
// Patched (ui-foundation): upstream keeps the tooltip on mobile and only
|
|
186
|
+
// hides its content, so keyboard focus still opened an invisible tooltip
|
|
187
|
+
// and the first Escape closed that instead of the sidebar sheet. On
|
|
188
|
+
// mobile the label is always visible, so there is no tooltip at all.
|
|
189
|
+
const hasTooltip = !!tooltip && !isMobile;
|
|
185
190
|
const comp = useRender({
|
|
186
191
|
defaultTagName: "button",
|
|
187
192
|
props: mergeProps({
|
|
188
193
|
className: cn(sidebarMenuButtonVariants({ variant, size }), className),
|
|
189
194
|
}, props),
|
|
190
|
-
render: !
|
|
195
|
+
render: !hasTooltip ? render : _jsx(TooltipTrigger, { render: render }),
|
|
191
196
|
state: {
|
|
192
197
|
slot: "sidebar-menu-button",
|
|
193
198
|
sidebar: "menu-button",
|
|
@@ -195,7 +200,7 @@ function SidebarMenuButton({ render, isActive = false, variant = "default", size
|
|
|
195
200
|
active: isActive,
|
|
196
201
|
},
|
|
197
202
|
});
|
|
198
|
-
if (!
|
|
203
|
+
if (!hasTooltip) {
|
|
199
204
|
return comp;
|
|
200
205
|
}
|
|
201
206
|
if (typeof tooltip === "string") {
|
|
@@ -203,7 +208,7 @@ function SidebarMenuButton({ render, isActive = false, variant = "default", size
|
|
|
203
208
|
children: tooltip,
|
|
204
209
|
};
|
|
205
210
|
}
|
|
206
|
-
return (_jsxs(Tooltip, { children: [comp, _jsx(TooltipContent, { side: "right", align: "center", hidden: state !== "collapsed"
|
|
211
|
+
return (_jsxs(Tooltip, { children: [comp, _jsx(TooltipContent, { side: "right", align: "center", hidden: state !== "collapsed", ...tooltip })] }));
|
|
207
212
|
}
|
|
208
213
|
function SidebarMenuAction({ className, render, showOnHover = false, ...props }) {
|
|
209
214
|
return useRender({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/styles/index.css
CHANGED
|
@@ -44,6 +44,9 @@
|
|
|
44
44
|
--color-warning: var(--warning);
|
|
45
45
|
--color-warning-foreground: var(--warning-foreground);
|
|
46
46
|
--color-warning-text: var(--warning-text);
|
|
47
|
+
--color-info: var(--info);
|
|
48
|
+
--color-info-foreground: var(--info-foreground);
|
|
49
|
+
--color-info-text: var(--info-text);
|
|
47
50
|
--color-category-1: var(--category-1);
|
|
48
51
|
--color-category-2: var(--category-2);
|
|
49
52
|
--color-category-3: var(--category-3);
|
package/styles/theme.css
CHANGED
|
@@ -53,6 +53,9 @@
|
|
|
53
53
|
--amber-700: oklch(0.555 0.163 48.998); /* white on it: 5.05 */
|
|
54
54
|
--amber-800: oklch(0.47 0.14 48.998); /* on white: 7.20 */
|
|
55
55
|
--amber-400: oklch(0.828 0.189 84.429); /* on near-black: 11.53 */
|
|
56
|
+
--blue-700: oklch(0.52 0.17 255); /* white on it: 5.59 */
|
|
57
|
+
--blue-800: oklch(0.45 0.15 255); /* on white: 7.54 */
|
|
58
|
+
--blue-400: oklch(0.75 0.12 255); /* on near-black: 8.90 */
|
|
56
59
|
/*
|
|
57
60
|
* Categorical slots. For values that differ from each other but are not
|
|
58
61
|
* better or worse than each other — a genre, a team, a category. Tones
|
|
@@ -65,7 +68,8 @@
|
|
|
65
68
|
*
|
|
66
69
|
* Hues avoid the red and green bands that destructive/success already
|
|
67
70
|
* spend, and the amber that warning does — a category drawn in red reads
|
|
68
|
-
* as "bad".
|
|
71
|
+
* as "bad". The blue slots (1, 6, 8) sit near info's hue, which came
|
|
72
|
+
* later: a table using info keeps its categories off those three. They are ordered by distinctness, not by hue angle, so an app
|
|
69
73
|
* using only the first three gets three obviously different colours.
|
|
70
74
|
* Past about five, hue alone stops separating them and the glyph has to
|
|
71
75
|
* carry the meaning; see docs/design-language.md.
|
|
@@ -116,7 +120,9 @@
|
|
|
116
120
|
--success-foreground: var(--white);
|
|
117
121
|
--warning: var(--amber-700);
|
|
118
122
|
--warning-foreground: var(--white);
|
|
119
|
-
|
|
123
|
+
--info: var(--blue-700);
|
|
124
|
+
--info-foreground: var(--white);
|
|
125
|
+
/* The four pairs above are a SOLID fill plus its own text color, and
|
|
120
126
|
nothing else. Used as text or a border on the page's own background —
|
|
121
127
|
an outline badge, a tinted chip, an icon tint — a fill shade is the
|
|
122
128
|
wrong value: it was picked to carry white, not to read against the
|
|
@@ -125,6 +131,7 @@
|
|
|
125
131
|
--destructive-text: var(--red-700);
|
|
126
132
|
--success-text: var(--green-800);
|
|
127
133
|
--warning-text: var(--amber-800);
|
|
134
|
+
--info-text: var(--blue-800);
|
|
128
135
|
/* Categorical slots — a glyph tint, never a fill behind text. */
|
|
129
136
|
--category-1: var(--category-1-700);
|
|
130
137
|
--category-2: var(--category-2-700);
|
|
@@ -175,12 +182,15 @@
|
|
|
175
182
|
--success-foreground: var(--white);
|
|
176
183
|
--warning: var(--amber-700);
|
|
177
184
|
--warning-foreground: var(--white);
|
|
185
|
+
--info: var(--blue-700);
|
|
186
|
+
--info-foreground: var(--white);
|
|
178
187
|
/* Solid fills keep one shade in both themes — they always carry white
|
|
179
188
|
text, so the page behind them never enters the calculation. Only the
|
|
180
189
|
*-text shades flip, because those read against the page. */
|
|
181
190
|
--destructive-text: var(--red-400);
|
|
182
191
|
--success-text: var(--green-400);
|
|
183
192
|
--warning-text: var(--amber-400);
|
|
193
|
+
--info-text: var(--blue-400);
|
|
184
194
|
/* Categorical slots — lighter shades to read against the dark page. */
|
|
185
195
|
--category-1: var(--category-1-400);
|
|
186
196
|
--category-2: var(--category-2-400);
|