@tristan2828/ui-foundation 3.27.4 → 3.28.1
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/docs/design-language.md +34 -15
- package/dist/components/app/data-table.js +1 -1
- package/dist/components/app/entity-view.js +3 -3
- package/dist/components/ui/button.js +1 -1
- package/dist/components/ui/calendar.js +2 -2
- package/dist/components/ui/dropdown-menu.js +1 -1
- package/dist/components/ui/sidebar.js +8 -3
- package/dist/components/ui/switch.js +1 -1
- package/dist/components/ui/toggle.js +1 -1
- package/package.json +1 -1
- package/styles/theme.css +56 -0
|
@@ -227,9 +227,9 @@ pulls the eye from every other column (cell patterns 16 and 20).
|
|
|
227
227
|
|
|
228
228
|
## Density
|
|
229
229
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
Four densities, set with `data-density` on **any ancestor** (a wrapper
|
|
231
|
+
`<div>`, a route's layout, or `<html>` for the whole app). It is a token
|
|
232
|
+
set, not a `DataTable` prop:
|
|
233
233
|
|
|
234
234
|
```tsx
|
|
235
235
|
<div data-density="compact">
|
|
@@ -237,18 +237,37 @@ a token set, not a `DataTable` prop:
|
|
|
237
237
|
</div>
|
|
238
238
|
```
|
|
239
239
|
|
|
240
|
-
| `data-density` |
|
|
241
|
-
|
|
242
|
-
| `compact` | 4px | 32px | wide tables people scan, many rows per screen |
|
|
243
|
-
| *(unset)* | 8px | 40px | the default |
|
|
244
|
-
| `comfortable` | 12px | 48px | short tables, or rows people read rather than scan |
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
240
|
+
| `data-density` | Changes | Table cell padding (vertical) | Header height | For |
|
|
241
|
+
|---|---|---|---|---|
|
|
242
|
+
| `compact` | table rows only | 4px | 32px | wide tables people scan, many rows per screen |
|
|
243
|
+
| *(unset)* | — | 8px | 40px | the default |
|
|
244
|
+
| `comfortable` | table rows only | 12px | 48px | short tables, or rows people read rather than scan |
|
|
245
|
+
| `large` | **everything**: type, spacing, control heights, icons, the sidebar, table rows | 12px | 56px | a screen read from a distance: a TV used as the desktop monitor, with a mouse and keyboard |
|
|
246
|
+
|
|
247
|
+
- `compact` and `comfortable` change padding only. Text keeps
|
|
248
|
+
`type-body`, so compact never drops below a readable size.
|
|
249
|
+
- `large` grows the whole UI by about a quarter: body text 14 → 18px,
|
|
250
|
+
controls 32 → 40px tall, icons 16 → 20px, the sidebar 256 → 320px.
|
|
251
|
+
Breakpoints and `max-w-*` containers stay where they are, which is
|
|
252
|
+
what browser zoom can't do (zoom narrows the viewport in CSS pixels, so
|
|
253
|
+
a wide screen can fall into the phone layout).
|
|
254
|
+
- For `large`, set it on **`<html>`**. Menus, selects, dialogs and
|
|
255
|
+
tooltips are portaled to `<body>`, so a wrapper `<div>` doesn't reach
|
|
256
|
+
them. `compact` and `comfortable` only touch tables, so a wrapper is fine.
|
|
248
257
|
- Pick **one density per screen**. Two tables at different densities side
|
|
249
258
|
by side look like a mistake.
|
|
259
|
+
- Size things the way the package does and `large` reaches them for free:
|
|
260
|
+
Tailwind's spacing and size utilities (`h-8`, `size-4`, `gap-2`, `w-64`)
|
|
261
|
+
and `text-*`/`type-*`. A fixed `h-[18px]`, `w-[240px]` or
|
|
262
|
+
`text-[13px]` stays its size at `large`; write it as a step of the scale
|
|
263
|
+
(`h-[--spacing(4.5)]`) instead.
|
|
250
264
|
- Under the hood, the table primitive's cells read `--table-cell-px`,
|
|
251
|
-
`--table-cell-py` and `--table-head-height
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
265
|
+
`--table-cell-py` and `--table-head-height`. `large` also redefines
|
|
266
|
+
Tailwind's own scale (`--spacing`, `--text-*`, `--radius`), the `--type-*`
|
|
267
|
+
roles and `--control-text-sm` (the small Button and Toggle). Each
|
|
268
|
+
default is the value it always had, so a screen without `data-density`
|
|
269
|
+
doesn't change. The selectors are in `styles/theme.css`. Storybook's
|
|
270
|
+
Density toolbar shows any story at a density, and `patterns/Density`
|
|
271
|
+
shows the primitives, `DataTable` and the sidebar at each. Its checks
|
|
272
|
+
measure that every step changes what it should, and that the default
|
|
273
|
+
keeps today's sizes.
|
|
@@ -79,7 +79,7 @@ const ROW_GROUP_CLASS = 'group/row hover:bg-transparent';
|
|
|
79
79
|
// width it last had loaded while it shows another state
|
|
80
80
|
// (--data-table-last-width), at most the area. Narrower, no change.
|
|
81
81
|
const CONTENT_WIDTH_CLASS = '@7xl/data-table:w-fit @7xl/data-table:max-w-full ' +
|
|
82
|
-
'@7xl/data-table:min-w-[max(
|
|
82
|
+
'@7xl/data-table:min-w-[max(--spacing(144),var(--data-table-last-width,0px))]';
|
|
83
83
|
// A second horizontal scrollbar, stuck to the viewport's bottom edge. The
|
|
84
84
|
// real one sits directly under the last row, which on a full page is
|
|
85
85
|
// off-screen until you have already scrolled past every row. Only rendered
|
|
@@ -67,8 +67,8 @@ function orEmptyLabel(value, emptyLabel) {
|
|
|
67
67
|
// <dl>): label above value in the narrow rail, beside it once the rail's
|
|
68
68
|
// sections stack full width under the header.
|
|
69
69
|
const FIELD_ROW = {
|
|
70
|
-
page: 'sm:grid sm:grid-cols-[minmax(
|
|
71
|
-
rail: '@lg/fields:grid @lg/fields:grid-cols-[minmax(
|
|
70
|
+
page: 'sm:grid sm:grid-cols-[minmax(--spacing(32),--spacing(48))_1fr] sm:gap-6',
|
|
71
|
+
rail: '@lg/fields:grid @lg/fields:grid-cols-[minmax(--spacing(32),--spacing(48))_1fr] @lg/fields:gap-6',
|
|
72
72
|
};
|
|
73
73
|
function FieldRow({ field, inRail }) {
|
|
74
74
|
return (_jsxs("div", { className: cn('flex flex-col gap-1 py-3 first:pt-0 last:pb-0', FIELD_ROW[inRail ? 'rail' : 'page']), children: [_jsx("dt", { className: "type-label text-muted-foreground", children: field.label }), _jsx("dd", { className: "min-w-0 type-body break-words text-foreground", children: field.edit ? (_jsx(EditableValue, { label: field.label, edit: field.edit, children: orEmptyLabel(field.value, field.emptyLabel) })) : (orEmptyLabel(field.value, field.emptyLabel)) })] }));
|
|
@@ -85,7 +85,7 @@ function ViewSection({ section, inRail = false }) {
|
|
|
85
85
|
// right of row 1 and main on its left, both starting under the header.
|
|
86
86
|
const RAIL_LAYOUT = {
|
|
87
87
|
page: '@container/view flex min-w-0 flex-col gap-6',
|
|
88
|
-
columns: 'flex flex-col gap-6 @4xl/view:grid @4xl/view:grid-cols-[minmax(0,1fr)
|
|
88
|
+
columns: 'flex flex-col gap-6 @4xl/view:grid @4xl/view:grid-cols-[minmax(0,1fr)_--spacing(88)] @4xl/view:items-start',
|
|
89
89
|
rail: '@4xl/view:col-start-2 @4xl/view:row-start-1',
|
|
90
90
|
main: 'flex min-w-0 flex-col gap-6 @4xl/view:col-start-1 @4xl/view:row-start-1',
|
|
91
91
|
// Main alone (no rail sections): the whole row.
|
|
@@ -15,7 +15,7 @@ const buttonVariants = cva("group/button inline-flex shrink-0 items-center justi
|
|
|
15
15
|
size: {
|
|
16
16
|
default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
17
17
|
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
18
|
-
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-
|
|
18
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-(length:--control-text-sm) in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
19
19
|
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
20
20
|
icon: "size-8",
|
|
21
21
|
"icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
@@ -26,10 +26,10 @@ function Calendar({ className, classNames, showOutsideDays = true, captionLayout
|
|
|
26
26
|
: "flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground", defaultClassNames.caption_label),
|
|
27
27
|
month_grid: cn("w-full border-collapse", defaultClassNames.month_grid),
|
|
28
28
|
weekdays: cn("flex", defaultClassNames.weekdays),
|
|
29
|
-
weekday: cn("flex-1 rounded-(--cell-radius) text-
|
|
29
|
+
weekday: cn("flex-1 rounded-(--cell-radius) text-(length:--control-text-sm) font-normal text-muted-foreground select-none", defaultClassNames.weekday),
|
|
30
30
|
week: cn("mt-2 flex w-full", defaultClassNames.week),
|
|
31
31
|
week_number_header: cn("w-(--cell-size) select-none", defaultClassNames.week_number_header),
|
|
32
|
-
week_number: cn("text-
|
|
32
|
+
week_number: cn("text-(length:--control-text-sm) text-muted-foreground select-none", defaultClassNames.week_number),
|
|
33
33
|
day: cn("group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)", props.showWeekNumber
|
|
34
34
|
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)"
|
|
35
35
|
: "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)", defaultClassNames.day),
|
|
@@ -31,7 +31,7 @@ function DropdownMenuSubTrigger({ className, inset, children, ...props }) {
|
|
|
31
31
|
return (_jsxs(MenuPrimitive.SubmenuTrigger, { "data-slot": "dropdown-menu-sub-trigger", "data-inset": inset, className: cn("flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props, children: [children, _jsx(ChevronRightIcon, { className: "ml-auto" })] }));
|
|
32
32
|
}
|
|
33
33
|
function DropdownMenuSubContent({ align = "start", alignOffset = -3, side = "right", sideOffset = 0, className, ...props }) {
|
|
34
|
-
return (_jsx(DropdownMenuContent, { "data-slot": "dropdown-menu-sub-content", className: cn("w-auto min-w-
|
|
34
|
+
return (_jsx(DropdownMenuContent, { "data-slot": "dropdown-menu-sub-content", className: cn("w-auto min-w-24 rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className), align: align, alignOffset: alignOffset, side: side, sideOffset: sideOffset, ...props }));
|
|
35
35
|
}
|
|
36
36
|
function DropdownMenuCheckboxItem({ className, children, checked, inset, ...props }) {
|
|
37
37
|
return (_jsxs(MenuPrimitive.CheckboxItem, { "data-slot": "dropdown-menu-checkbox-item", "data-inset": inset, className: cn("relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), checked: checked, ...props, children: [_jsx("span", { className: "pointer-events-none absolute right-2 flex items-center justify-center", "data-slot": "dropdown-menu-checkbox-item-indicator", children: _jsx(MenuPrimitive.CheckboxItemIndicator, { children: _jsx(CheckIcon, {}) }) }), children] }));
|
|
@@ -14,9 +14,14 @@ import { Tooltip, TooltipContent, TooltipTrigger, } from "./tooltip.js";
|
|
|
14
14
|
import { PanelLeftIcon } from "lucide-react";
|
|
15
15
|
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
|
16
16
|
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
// This repo's patch: the widths are steps of Tailwind's spacing scale, not
|
|
18
|
+
// rem literals (upstream's 16rem, 18rem, 3rem), so data-density="large"
|
|
19
|
+
// widens the sidebar with everything in it and the icon rail still fits
|
|
20
|
+
// its buttons (styles/theme.css). Diff against upstream before taking a
|
|
21
|
+
// new version of this file.
|
|
22
|
+
const SIDEBAR_WIDTH = "calc(var(--spacing) * 64)";
|
|
23
|
+
const SIDEBAR_WIDTH_MOBILE = "calc(var(--spacing) * 72)";
|
|
24
|
+
const SIDEBAR_WIDTH_ICON = "calc(var(--spacing) * 12)";
|
|
20
25
|
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
|
21
26
|
const SidebarContext = React.createContext(null);
|
|
22
27
|
function useSidebar() {
|
|
@@ -2,6 +2,6 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
|
|
3
3
|
import { cn } from "cn";
|
|
4
4
|
function Switch({ className, size = "default", ...props }) {
|
|
5
|
-
return (_jsx(SwitchPrimitive.Root, { "data-slot": "switch", "data-size": size, className: cn("peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none group-has-[:focus-visible]/field-label:border-transparent group-has-[:focus-visible]/field-label:ring-0 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[
|
|
5
|
+
return (_jsx(SwitchPrimitive.Root, { "data-slot": "switch", "data-size": size, className: cn("peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none group-has-[:focus-visible]/field-label:border-transparent group-has-[:focus-visible]/field-label:ring-0 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[--spacing(4.6)] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50", className), ...props, children: _jsx(SwitchPrimitive.Thumb, { "data-slot": "switch-thumb", className: "pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground" }) }));
|
|
6
6
|
}
|
|
7
7
|
export { Switch };
|
|
@@ -15,7 +15,7 @@ const toggleVariants = cva("group/toggle inline-flex items-center justify-center
|
|
|
15
15
|
},
|
|
16
16
|
size: {
|
|
17
17
|
default: "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
18
|
-
sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-
|
|
18
|
+
sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-(length:--control-text-sm) has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
19
19
|
lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
20
20
|
// 24px: the smallest target WCAG 2.2 allows (2.5.8).
|
|
21
21
|
"icon-xs": "size-6 min-w-6 rounded-[min(var(--radius-md),10px)] [&_svg:not([class*='size-'])]:size-3.5",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.28.1",
|
|
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/theme.css
CHANGED
|
@@ -164,6 +164,10 @@
|
|
|
164
164
|
--table-cell-px: 0.5rem;
|
|
165
165
|
--table-cell-py: 0.5rem;
|
|
166
166
|
--table-head-height: 2.5rem;
|
|
167
|
+
/* The small size of Button and Toggle (and the calendar's weekdays):
|
|
168
|
+
between text-xs and text-sm, so it has a token of its own for the
|
|
169
|
+
large density to scale. */
|
|
170
|
+
--control-text-sm: 0.8rem;
|
|
167
171
|
|
|
168
172
|
/* Layer 2 — semantic (light) */
|
|
169
173
|
--background: var(--white);
|
|
@@ -251,6 +255,58 @@
|
|
|
251
255
|
--table-head-height: 3rem;
|
|
252
256
|
}
|
|
253
257
|
|
|
258
|
+
/*
|
|
259
|
+
* Large: a screen read from a distance (a TV used as the desktop monitor,
|
|
260
|
+
* with a mouse and keyboard). Not only tables: everything under it grows,
|
|
261
|
+
* by about a quarter, while the layout's breakpoints stay where they are
|
|
262
|
+
* (browser zoom would move them and switch a wide screen to the phone
|
|
263
|
+
* layout).
|
|
264
|
+
*
|
|
265
|
+
* Most of it is Tailwind's own scale. Every spacing, size and icon utility
|
|
266
|
+
* compiles to calc(var(--spacing) * n) and every text-* to var(--text-*),
|
|
267
|
+
* so redefining those here reaches every primitive, composite and app
|
|
268
|
+
* class at once. The primitives that sized something outside that scale
|
|
269
|
+
* (the sidebar's width, the switch, the small control text) were patched
|
|
270
|
+
* to read it. Each value below is a literal, never var() of another
|
|
271
|
+
* token: a custom property is resolved where it is declared, so one
|
|
272
|
+
* built from --spacing on :root would keep :root's value under here.
|
|
273
|
+
*
|
|
274
|
+
* Popups (menus, selects, dialogs, tooltips) are portaled to <body>, so
|
|
275
|
+
* they only grow when data-density="large" is on <html>.
|
|
276
|
+
*/
|
|
277
|
+
[data-density="large"] {
|
|
278
|
+
--spacing: 0.3125rem; /* 4px → 5px: h-8 is 40px, size-4 is 20px */
|
|
279
|
+
--text-xs: 0.9375rem; /* 12 → 15px */
|
|
280
|
+
--text-sm: 1.125rem; /* 14 → 18px: most UI text */
|
|
281
|
+
--text-base: 1.25rem; /* 16 → 20px */
|
|
282
|
+
--text-lg: 1.375rem; /* 18 → 22px */
|
|
283
|
+
--text-xl: 1.5625rem; /* 20 → 25px */
|
|
284
|
+
--text-2xl: 1.875rem; /* 24 → 30px */
|
|
285
|
+
--control-text-sm: 1rem; /* 12.8 → 16px */
|
|
286
|
+
--radius: 0.75rem; /* 10 → 12px, so larger controls keep their shape */
|
|
287
|
+
|
|
288
|
+
--type-page-title-size: 1.375rem;
|
|
289
|
+
--type-page-title-leading: 2.125rem;
|
|
290
|
+
--type-section-title-size: 1.25rem;
|
|
291
|
+
--type-section-title-leading: 1.875rem;
|
|
292
|
+
--type-body-size: 1.125rem;
|
|
293
|
+
--type-body-leading: 1.625rem;
|
|
294
|
+
--type-label-size: 1.125rem;
|
|
295
|
+
--type-label-leading: 1.625rem;
|
|
296
|
+
--type-caption-size: 0.9375rem;
|
|
297
|
+
--type-caption-leading: 1.25rem;
|
|
298
|
+
--type-heading-1-size: 2rem;
|
|
299
|
+
--type-heading-1-leading: 2.5rem;
|
|
300
|
+
--type-heading-2-size: 1.5625rem;
|
|
301
|
+
--type-heading-2-leading: 2.125rem;
|
|
302
|
+
--type-heading-3-size: 1.3125rem;
|
|
303
|
+
--type-heading-3-leading: 1.875rem;
|
|
304
|
+
|
|
305
|
+
--table-cell-px: 0.75rem;
|
|
306
|
+
--table-cell-py: 0.75rem;
|
|
307
|
+
--table-head-height: 3.5rem;
|
|
308
|
+
}
|
|
309
|
+
|
|
254
310
|
.dark {
|
|
255
311
|
/* Layer 2 — semantic (dark). Only this block is remapped; Layer 1 is not. */
|
|
256
312
|
--background: var(--black);
|