@nxgt/material 3.22.0 → 3.24.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +5 -4
  3. package/dist/backgrounds/css/motifs.css +12 -11
  4. package/dist/backgrounds/css-react/icons.css +9 -10
  5. package/dist/components/theme-tokens.css +12 -0
  6. package/dist/components/ui/layout/app-shell/activity-backdrop.css +3 -16
  7. package/dist/{components-B0oezhGe.js → components-D2XNfS3c.js} +12872 -12820
  8. package/dist/components.js +1 -1
  9. package/dist/lib/components/ui/collections/data-table/partials/data-table-page-control.d.ts +7 -2
  10. package/dist/lib/components/ui/collections/data-table/partials/data-table-toolbar.d.ts +6 -2
  11. package/dist/lib/components/ui/collections/data-table/types.d.ts +4 -0
  12. package/dist/lib/components/ui/collections/data-table/use-pager-focus.d.ts +30 -0
  13. package/dist/lib/components/ui/collections/server-table/partials/server-table-band-classes.d.ts +6 -0
  14. package/dist/lib/components/ui/collections/server-table/partials/server-table-band.d.ts +3 -1
  15. package/dist/lib/components/ui/collections/server-table/types.d.ts +3 -0
  16. package/dist/main.js +1 -1
  17. package/dist/material.css +1 -1
  18. package/dist/styles.css +1 -0
  19. package/docs/components/data-table-filter-and-lists.md +24 -2
  20. package/docs/components/navigation-and-layout.md +2 -2
  21. package/docs/guides/app-shell.md +17 -1
  22. package/docs/guides/theming.md +40 -0
  23. package/docs/previews/navigation-layout-threadlayout.webp +0 -0
  24. package/docs/troubleshooting.md +47 -0
  25. package/lib/backgrounds/css/motifs.css +12 -11
  26. package/lib/backgrounds/css-react/icons.css +9 -10
  27. package/lib/components/theme-tokens.css +12 -0
  28. package/lib/components/ui/layout/app-shell/activity-backdrop.css +3 -16
  29. package/lib/styles.css +1 -0
  30. package/lib/themes/ambre.css +3 -1
  31. package/lib/themes/ardoise.css +3 -1
  32. package/lib/themes/bleumono.css +3 -1
  33. package/lib/themes/bluemoon.css +3 -1
  34. package/lib/themes/corail.css +3 -1
  35. package/lib/themes/foret.css +3 -1
  36. package/lib/themes/lavande.css +3 -1
  37. package/lib/themes/menthe.css +3 -1
  38. package/lib/themes/ocean.css +3 -1
  39. package/lib/themes/pink.css +3 -1
  40. package/lib/themes/prune.css +3 -1
  41. package/lib/themes/sable.css +3 -1
  42. package/lib/themes/slate.css +3 -1
  43. package/lib/themes/teal.css +3 -1
  44. package/package.json +2 -2
package/dist/styles.css CHANGED
@@ -52,6 +52,7 @@
52
52
  --color-paper: --alpha(var(--primary) / 0.5%);
53
53
  --color-strix: var(--strix);
54
54
  --color-background: var(--background);
55
+ --color-ground: var(--ground);
55
56
  --color-background-paper: --alpha(var(--primary) / 5%);
56
57
  --color-background-light: --alpha(var(--background) / 30%);
57
58
  --color-foreground: var(--foreground);
@@ -122,6 +122,7 @@ table enables, for typing your own column defs.
122
122
  | `getRowHref` | `(row) => string \| undefined` | — | the first data cell links to the row's page; a click elsewhere on the row follows it |
123
123
  | `onRowClick` | `(row, event) => void` | — | a click on the row (not on a control); alone (without `getRowHref`), it makes the row focusable and Enter calls it |
124
124
  | `linkComponent` | `ComponentType<{ href, className?, children?, rel?, 'aria-label'?, onClick? }>` | `<a>` | the link for `getRowHref` and the page links: wrap your router's. It must forward `className` and `ref` (the past-end page's "Back to the first page" is a `Button asChild` over it) |
125
+ | `reserveActions` | `boolean` | `true` | with `actions` and a `select` column: keep the toolbar's 68px band while nothing is selected, so the bar does not push the rows down; `false` keeps none and the bar arrives on the first selection (`ServerTable` passes `false`) |
125
126
  | `focusable` | `boolean` | `true` with `actions`, else `false` | the `<table>` (or the list of cards or tiles) takes the focus by script (`tabindex="-1"`, never a tab stop): where the focus goes once the control that had it is gone. When the selection is cleared and the focused control went with it (a bulk action's button), the table takes the focus instead of the page's body ([focus after a bulk action](#focus-after-a-bulk-action)); `false` turns both off |
126
127
  | `tableRef` | `Ref<HTMLTableElement>` | — | the `<table>`, to focus it yourself (with `focusable`); `controls.focusTable()` does the same from a toolbar slot. It stays `null` while the layout is a list (`cards`, `tiles`, or the phone side of `{ phone, wide }`): use `focusRef` |
127
128
  | `focusRef` | `Ref<HTMLElement>` | — | the element `focusable` moves the focus to, whichever the layout: the `<table>`, or the list of cards |
@@ -273,6 +274,18 @@ wide screen; on a phone the extra lines still push them down).
273
274
  />
274
275
  ```
275
276
 
277
+ ### Focus while paging
278
+
279
+ A page control that has the focus keeps it while the next page loads: a link
280
+ (or a button) stays the same element, `aria-disabled`, dimmed, and a click on
281
+ it does nothing, instead of being swapped for a disabled button (which drops the
282
+ focus to `<body>`, #1609). When a control really goes (Next on the last page),
283
+ the focus moves to its counterpart, or to the pagination `<nav>`
284
+ (`tabindex="-1"`, never a tab stop) when neither is left. A focus or a pointer
285
+ press elsewhere is never pulled back. The footer's selected count is a polite
286
+ status, so the first selection is announced ("1 row selected") when its bar
287
+ shows up.
288
+
276
289
  ### Focus after a bulk action
277
290
 
278
291
  When a bulk action clears the selection, its buttons unmount and the focus
@@ -387,7 +400,10 @@ same minimum height once they do — so the "Delete selected" that appears on
387
400
  the first selection does not push the tapped row down. The reserved height
388
401
  is one row of default-size buttons (`h-9`); taller or wrapping actions still
389
402
  grow the toolbar. Without the `select` column, or without `actions`, no band
390
- is kept above the table.
403
+ is kept above the table. `reserveActions={false}` keeps none either: the bar
404
+ arrives on the first selection with a short grow (none under reduced motion),
405
+ and the rows move down by its height. `ServerTable` passes `false`, so its
406
+ bulk bar leaves no empty row under the band while nothing is selected.
391
407
 
392
408
  ```tsx
393
409
  <DataTable
@@ -868,7 +884,8 @@ Its props are `ServerTableProps<T>`; `filter` is a `ServerTableFilter`,
868
884
  | `description` | `ReactNode` | — | a muted line under the `title`, or alone above the band's controls without one; the `actions` stay at the band's end |
869
885
  | `actions` | `ReactNode` | — | the page's actions, at the band's end |
870
886
  | `bandDensity` | `'comfortable' \| 'compact'` | `'comfortable'` | `'compact'` halves the room above the band (16px to 8px) and under it (12px to 6px), for a list under a sticky app bar and a breadcrumb; the controls keep their size (the Filter's own `density` is the controls') |
871
- | `pageControls` | `'labelled' \| 'icon'` | `'labelled'` | the footer's Previous / Next: `'icon'` is icon-only, with a tooltip and the names "Previous page" / "Next page" (`DataTable`'s default is unchanged) |
887
+ | `pageControls` | `'labelled' \| 'icon'` | `'labelled'` | the footer's Previous / Next: `'icon'` is icon-only, with a tooltip and the names "Previous page" / "Next page" (`DataTable`'s default is unchanged); on a coarse pointer the icon controls are 44x44 boxes (still painted 32px) |
888
+ | `bandActions` | `'stacked' \| 'inline'` | `'stacked'` | with `filter`, where `actions` sit below 768px: `'stacked'` gives them a row above the search; `'inline'` keeps them on the search's row (the search takes the room left), for a lone icon action such as a "+". An action wider than 3.5rem wraps under the search. From 768px both are the same |
872
889
  | `filterRef` | `Ref<ServerTableFilterHandle>` | — | with `filter`: `filterRef.current.focus()` focuses the Filter's first control, the search, else the first quick filter, "Add filter" or "Filters (N)" the width shows |
873
890
  | `sorting` | `createSorting(…)` | — | headers sorted from the URL |
874
891
  | `filters` | `tableFilters(model)` | — | without `filter`: "nothing matches" and its clear |
@@ -883,6 +900,11 @@ Its props are `ServerTableProps<T>`; `filter` is a `ServerTableFilter`,
883
900
  | `resultsLabel` | `(count) => string` | "N results" | without `filter`: what the list's region says. The page past the last has its own words: `material.server-table.past-end`, `past-end-hint`, `first-page` and `past-end-status` |
884
901
  | `className` | `string` | — | on the list's surface |
885
902
 
903
+ ```tsx
904
+ // A lone icon action stays on the search's row on a phone.
905
+ <ServerTable {...props} filter={{ model }} bandActions="inline" actions={<IconButton aria-label="New" tooltip="New"><Icon name="plus" /></IconButton>} />
906
+ ```
907
+
886
908
  **Where the focus goes.** After "Clear" on an empty page, or the last chip
887
909
  removed, the focus returns to the Filter's search; a bar without one falls to
888
910
  its first shown control (a quick filter, "Add filter", or "Filters (N)" on a
@@ -31,7 +31,7 @@ documentation-site shell. Everything is imported from
31
31
 
32
32
  ![ActivityLayout](../previews/navigation-layout-appshell.webp)
33
33
 
34
- App shell: sidebar built from `items`, a header with a mobile sheet, route breadcrumbs and a titled page. See the [app shell guide](../guides/app-shell.md).
34
+ App shell: sidebar built from `items`, a header with a mobile sheet, route breadcrumbs and a titled page. It paints the tinted page ground (`bg-ground`) under the content, with the sidebar and the app bar flat on `--background` and the cards on `bg-card`. See the [app shell guide](../guides/app-shell.md).
35
35
 
36
36
  ```tsx
37
37
  import { Outlet } from 'react-router';
@@ -414,7 +414,7 @@ open the pane (`ListDetailsTrigger` takes the `view` to show), and their `childr
414
414
  layout state. A server, which has no screen width, renders the wide layout,
415
415
  as does a hydrating client; a phone switches right after hydrating. A page
416
416
  rendered only in the browser starts on the real layout. `ThreadLayout` is a centred column over the app
417
- [background](#background) for focused flows (sign-in, onboarding), with no
417
+ [background](#background) on the page ground (`bg-ground`, so a `bg-card` card stands apart) for focused flows (sign-in, onboarding), with no
418
418
  app bar.
419
419
 
420
420
  | Prop | Type | Default | Effect |
@@ -209,7 +209,7 @@ partitionMenu(items); // { content, footer }
209
209
  | `align` | `'start' \| 'center'` | `'start'` | Where the collapsed rail's entries sit: right under the brand, or centred in the height below it |
210
210
  | `menuOpen` | `boolean` | — | Controls the phone's menu sheet: open while `true`. Leave it out and the layout manages the sheet. Only a phone shows the sheet |
211
211
  | `onMenuOpenChange` | `(open: boolean) => void` | — | Called for every change of the sheet: the menu button, Escape, the close button, a followed link, a new location |
212
- | `backdrop` | `ReactNode \| false` | — | An exception to the theme's background, which the layout draws by default (Lavande's: the built-in gradient and blobs). A node replaces it for this screen: usually a `Background` preset from `@nxgt/material/backgrounds` (see [Use a background preset behind the shell](#use-a-background-preset-behind-the-shell)); `false` draws no background, a plain page |
212
+ | `backdrop` | `ReactNode \| false` | — | An exception to the theme's background (the page ground, [`--ground`](#the-page-ground), is painted either way), which the layout draws by default (Lavande's: the built-in gradient and blobs). A node replaces it for this screen: usually a `Background` preset from `@nxgt/material/backgrounds` (see [Use a background preset behind the shell](#use-a-background-preset-behind-the-shell)); `false` draws no background, a plain page |
213
213
 
214
214
  On a page with `viewport-fit=cover` (an installed PWA drawing under a
215
215
  notch and the home indicator), the shell pads clear of
@@ -502,6 +502,22 @@ while the sheet is open, so leave ids out of it. A link in `brand` (a home
502
502
  link around the logo) is the sheet's first focusable element, so it takes
503
503
  the focus when the sheet opens.
504
504
 
505
+ ## The page ground
506
+
507
+ The layout paints `bg-ground` (the `--ground` token, see [Page ground](./theming.md#page-ground))
508
+ on its root, so the content, the gutters and the space between the chrome and
509
+ the content are tinted, with or without a `backdrop`, in light and dark. The
510
+ sidebar and the app bar stay on `--background`: flat rectangles with no radius
511
+ and no shadow (translucent over a backdrop, about 90 % without one), so the edge
512
+ between chrome and content is the change of colour. The cards, and the
513
+ `ActivityContent` header card (`bg-card`), stand on the ground with their
514
+ hairline and soft shadow. With `scroll="container"` the same ground stays behind
515
+ the scrolling region.
516
+
517
+ `--ground: var(--background)` on the layout or an ancestor opts out. An app
518
+ sets no page colour of its own under the layout, and no colour on a backdrop;
519
+ `--ground` is the one place to tune it.
520
+
505
521
  ## The theme's background
506
522
 
507
523
  `ActivityLayout` draws the background of the nearest theme (`NxgtThemeProvider`
@@ -72,6 +72,7 @@ at the top are needed is in [getting started](../getting-started.md#styles).
72
72
  | `--primary-indicator` | `bg-primary-indicator`, `border-primary-indicator`, `ring-primary-indicator`, `stroke-primary-indicator` | primary drawn as a non-text mark that carries a state (WCAG 1.4.11, 3:1): an active bar or dot, a selected or focused border, a ring, a spinner arc. Equal to `--primary` in light; in dark, `--primary` mixed 25 % with white (`color-mix(in oklab, var(--primary) 75%, white)`), 4.8:1 on `--background`, 4.5:1 on `--card`, 4.2:1 on `--muted` (`--primary` itself gives 2.7, 2.5 and 2.3:1) |
73
73
  | `--error-text` | `text-error-text` | error drawn as text: field messages, `HelperText` in its error state, form and dialog errors, `variant="error"` menu items |
74
74
  | `--destructive` | `bg-destructive`, `text-destructive` | deprecated alias of `--error`: the light theme sets it to `var(--error)`, the dark theme keeps its own value; use `--error` |
75
+ | `--ground` | `bg-ground` | the page's ground under `ActivityLayout`: the tinted colour the cards stand on (see [Page ground](#page-ground)) |
75
76
  | `--card`, `--popover`, `--tooltip` (+ `-foreground`) | `bg-card`, `bg-popover`, `bg-tooltip` | surfaces |
76
77
  | `--border`, `--input`, `--ring` | `border-border`, `border-input`, `ring-ring` | outlines |
77
78
  | `--chart-1` … `--chart-5` | `bg-chart-1`, … | chart series (the charts read them at runtime) |
@@ -95,6 +96,45 @@ container is the accent colour itself, the same as `bg-accent`.
95
96
 
96
97
  Put `text-foreground` on a container; it stays well above 4.5:1 in both themes.
97
98
 
99
+ ## Page ground
100
+
101
+ `--ground` (`bg-ground`) is the colour of the page under the content: the page,
102
+ the gutters and what the app bar and the sidebar float on. `ActivityLayout`
103
+ paints it by default, with or without a `backdrop` (so does `ThreadLayout`), so a card (`bg-card`) stands
104
+ apart from the page in every screen and an app sets no page colour.
105
+
106
+ | Mode | Value | What it does |
107
+ | --- | --- | --- |
108
+ | light | `color-mix(in oklab, var(--primary) 7%, var(--background))` | the primary at 7 % over the page: a quiet tint, 0.03 to 0.04 ΔEOK from a white card |
109
+ | dark | `color-mix(in oklab, var(--primary) 6%, color-mix(in oklab, var(--background) 55%, black))` | darker than `--background` (lightness about 0.105 against 0.150) and than `--card`, so the cards lift off it: 0.08 to 0.09 ΔEOK |
110
+
111
+ The mixes are in `oklab`, never `oklch`: `oklch` skews the hue when a colour is
112
+ mixed with white. Every built-in theme and every `defineTheme` output carries
113
+ the token (light block and dark block), written from the theme's own
114
+ `--primary` and `--background`, so a zone gets its own ground. Text on it keeps
115
+ AA: foreground, muted foreground and primary text are measured on the ground in
116
+ both modes.
117
+
118
+ The sidebar and the app bar do **not** take the tint: they stay on
119
+ `--background` (near-white in light), flat, with no radius and no shadow, so
120
+ only the content ground carries the colour. The cards keep `bg-card`, their
121
+ hairline border and their soft shadow.
122
+
123
+ ```css
124
+ /* A custom tint, anywhere above the layout (or on a zone): */
125
+ :root { --ground: color-mix(in oklab, var(--primary) 10%, var(--background)); }
126
+
127
+ /* Opt out: the page is flat again, as before 3.24. */
128
+ [data-slot="activity-layout"] { --ground: var(--background); }
129
+ ```
130
+
131
+ A theme overrides it like any token: `defineTheme({ …, tokens: { light: { ground:
132
+ 'var(--background)' } } })`, and `checkThemeContrast` measures the override.
133
+
134
+ Do not paint your own page background under the layout (`bg-background`,
135
+ `bg-muted`, a gradient) or add a shadow to every card: both fight the ground.
136
+ A backdrop's colours come from the theme; set none on it.
137
+
98
138
  ## The neutral palette
99
139
 
100
140
  The default theme's page is plain: pure white in light and the one dark night
@@ -25,6 +25,7 @@ words in your error.
25
25
 
26
26
  **Styles & assets**
27
27
 
28
+ - [The page and the cards look the same](#the-page-and-the-cards-look-the-same)
28
29
  - [Components render unstyled, with no error](#components-render-unstyled-with-no-error)
29
30
  - [A link is blue and underlined on hover and focus](#a-link-of-mine-or-of-a-component-is-blue-and-underlined-on-hover-and-focus)
30
31
  - [The light and dark neutrals turned violet after upgrading to 3.6](#the-light-and-dark-neutrals-turned-violet-after-upgrading-to-36)
@@ -356,6 +357,8 @@ words in your error.
356
357
  - [A `ServerTable` search shows older text, or a write is lost, over a slow router](#a-servertable-search-shows-older-text-or-a-write-is-lost-over-a-slow-router)
357
358
  - [`ListEmpty`'s title is now a heading](#listempty-title-is-now-a-heading)
358
359
  - [`ServerTable` keeps showing the empty page or the old rows after a failed fetch](#servertable-keeps-showing-the-empty-page-or-the-old-rows-after-a-failed-fetch)
360
+ - [The focus falls to `<body>` after Previous / Next on a `ServerTable`](#the-focus-falls-to-body-after-previous--next-on-a-servertable)
361
+ - [An empty gap under the `ServerTable` band while nothing is selected](#an-empty-gap-under-the-servertable-band-while-nothing-is-selected)
359
362
  - [The focus falls to the page after a `DataTable` bulk action](#the-focus-falls-to-the-page-after-a-datatable-bulk-action)
360
363
  - [A test's `getByText('No data available')` finds two elements after a change](#a-tests-getbytextno-data-available-finds-two-elements-after-a-change)
361
364
  - [`` [@nxgt/material] DataTable: `virtual` needs `maxHeight`; the rows are not virtualized. ``](#nxgtmaterial-datatable-virtual-needs-maxheight-the-rows-are-not-virtualized)
@@ -708,6 +711,33 @@ Vite and React Router apps use:
708
711
 
709
712
  ## Styles & assets
710
713
 
714
+ ### The page and the cards look the same
715
+
716
+ **When:** a screen under `ActivityLayout`, after the 3.24 upgrade or on an older
717
+ setup. The cards, tiles and the header card have no visible edge against the
718
+ page.
719
+ **Why:** the page ground (`--ground`, `bg-ground`) is flat again, equal to
720
+ `--background`: an app's CSS paints its own page colour under the layout (a
721
+ `bg-background` or `bg-white` on a wrapper, `body`, a route), sets
722
+ `--ground: var(--background)`, a stylesheet loaded without `theme-tokens.css`
723
+ (no `--ground`, so `bg-ground` paints nothing), or a theme whose `tokens`
724
+ override made `ground` equal to `card`.
725
+ **Fix:** remove the page background under the layout, import
726
+ `@nxgt/material/styles.css` (it carries the token), and read the value on the
727
+ layout:
728
+
729
+ ```css
730
+ /* a custom tint is fine; a flat page is the opt-out */
731
+ :root { --ground: color-mix(in oklab, var(--primary) 7%, var(--background)); }
732
+ ```
733
+
734
+ ```ts
735
+ getComputedStyle(document.querySelector('[data-slot="activity-layout"]')!).backgroundColor;
736
+ ```
737
+
738
+ Also drop app-level card shadows: the cards already carry a hairline and a soft
739
+ shadow.
740
+
711
741
  ### Components render unstyled, with no error
712
742
 
713
743
  **When:** on the first page load. Components have their structure but no
@@ -6502,6 +6532,23 @@ const columns = [{ accessorKey: 'name', header: NameHeader }];
6502
6532
  **Why:** `ServerTable` cannot tell a failed fetch from an empty answer: `error` was never passed, or it was passed and never cleared (`null`) when the next answer landed, so the failure stays on screen.
6503
6533
  **Fix:** pass `error` while the fetch has failed, with `onRetry`, and set it back to `null` as soon as the next answer lands. See [ServerTable](components/data-table-filter-and-lists.md#servertable).
6504
6534
 
6535
+ ### The focus falls to `<body>` after Previous / Next on a `ServerTable`
6536
+
6537
+ Versions before `reserveActions` landed swapped a page link for a disabled
6538
+ button while the next page loaded, which dropped the focus to `<body>`
6539
+ (always on a phone's tiles). Upgrade: the control now stays, `aria-disabled`,
6540
+ and the focus moves to its counterpart when one goes (Next on the last page).
6541
+ See [Focus while paging](components/data-table-filter-and-lists.md#focus-while-paging).
6542
+ A custom `linkComponent` must render an element that can take the focus (an
6543
+ `<a href>`).
6544
+
6545
+ ### An empty gap under the `ServerTable` band while nothing is selected
6546
+
6547
+ Versions before `reserveActions` reserved the toolbar's 68px (`header-placeholder`) for the
6548
+ bulk actions. `ServerTable` now reserves nothing. On `DataTable` itself the
6549
+ default is still `reserveActions` `true`: pass `reserveActions={false}` to get
6550
+ the same.
6551
+
6505
6552
  ### The focus falls to the page after a `DataTable` bulk action
6506
6553
 
6507
6554
  **When:** a bulk action clears the selection, its buttons unmount, and the next Tab starts from the top of the page.
@@ -7,7 +7,10 @@
7
7
  @import "./scene.css";
8
8
 
9
9
  @layer components {
10
- /* The wash: two soft colour pools under the scene. */
10
+ /* The wash: one even, soft gradient over the whole layer, top to bottom, no
11
+ * pool in a corner. It sits in the backdrop layer under the content column, so
12
+ * it never repaints the sidebar or the app bar, and the page ground
13
+ * (--ground) shows through it. */
11
14
  :is(
12
15
  [data-background="shapes"],
13
16
  [data-background="confetti-grid"],
@@ -18,17 +21,15 @@
18
21
  ) > [data-layer="wash"] {
19
22
  position: absolute;
20
23
  inset: 0;
21
- background:
22
- radial-gradient(
23
- ellipse 75% 65% at 30% 20%,
24
- color-mix(in oklab, var(--nxgt-bg-c1) var(--nxgt-bg-a1), transparent),
25
- transparent 72%
24
+ background: linear-gradient(
25
+ 180deg in oklab,
26
+ color-mix(
27
+ in oklab,
28
+ var(--nxgt-bg-c1) calc(var(--nxgt-bg-a1) * 1.75),
29
+ transparent
26
30
  ),
27
- radial-gradient(
28
- ellipse 60% 60% at 85% 90%,
29
- color-mix(in oklab, var(--nxgt-bg-c3) var(--nxgt-bg-a1), transparent),
30
- transparent 70%
31
- );
31
+ color-mix(in oklab, var(--nxgt-bg-c3) var(--nxgt-bg-a1), transparent)
32
+ );
32
33
  }
33
34
 
34
35
  /* The scene clips the float, which scales it past the frame. */
@@ -23,20 +23,19 @@
23
23
  @import "./icons-tint.css";
24
24
 
25
25
  @layer components {
26
+ /* The wash: one even, soft gradient over the layer (see css/motifs.css). */
26
27
  [data-background="icons"] > [data-layer="wash"] {
27
28
  position: absolute;
28
29
  inset: 0;
29
- background:
30
- radial-gradient(
31
- ellipse 75% 65% at 22% 18%,
32
- color-mix(in oklab, var(--nxgt-bg-c1) var(--nxgt-bg-a1), transparent),
33
- transparent 72%
30
+ background: linear-gradient(
31
+ 180deg in oklab,
32
+ color-mix(
33
+ in oklab,
34
+ var(--nxgt-bg-c1) calc(var(--nxgt-bg-a1) * 1.75),
35
+ transparent
34
36
  ),
35
- radial-gradient(
36
- ellipse 60% 60% at 86% 90%,
37
- color-mix(in oklab, var(--nxgt-bg-c3) var(--nxgt-bg-a1), transparent),
38
- transparent 70%
39
- );
37
+ color-mix(in oklab, var(--nxgt-bg-c3) var(--nxgt-bg-a1), transparent)
38
+ );
40
39
  }
41
40
 
42
41
  :is([data-background="icons"], [data-background="shapes"]) > [data-scene] {
@@ -5,6 +5,11 @@
5
5
  --radius: 0.625rem;
6
6
  --background: oklch(1 0 0);
7
7
  --foreground: oklch(0.227 0.017 290);
8
+ /* The page ground under ActivityLayout (`bg-ground`): the primary at 7 % over the
9
+ page, so the cards (--card, white) stand apart from it. Set here, not on
10
+ `body`, so a zone that sets --primary or --background gets its own. A page
11
+ keeps the flat look with `--ground: var(--background)`. */
12
+ --ground: color-mix(in oklab, var(--primary) 7%, var(--background));
8
13
  --card: oklch(1 0 0);
9
14
  --card-foreground: oklch(0.227 0.017 290);
10
15
  --strix: oklch(45.91% 0.1864 310.75);
@@ -99,6 +104,13 @@
99
104
  [data-theme="dark"] {
100
105
  --background: oklch(0.15 0.026 273);
101
106
  --foreground: oklch(0.943 0.012 291.3);
107
+ /* Darker than --background so the cards (--card) stand apart: the page 55 %
108
+ toward black, the primary at 6 % over it. */
109
+ --ground: color-mix(
110
+ in oklab,
111
+ var(--primary) 6%,
112
+ color-mix(in oklab, var(--background) 55%, black)
113
+ );
102
114
  --card: oklch(0.19 0.019 289.4);
103
115
  --card-foreground: oklch(0.943 0.012 291.3);
104
116
  --popover: oklch(0.19 0.019 289.4);
@@ -6,8 +6,8 @@
6
6
  * Plain CSS, no inline `style`, in `@layer components` like the rest: an app's
7
7
  * utilities still win. The layer itself (`data-slot="activity-layout-backdrop"`)
8
8
  * is in the content column, positioned by classes in activity-layout.tsx: the
9
- * frame's padding and gaps take the chrome fill, so the shapes show nowhere
10
- * else.
9
+ * frame's padding and gaps show the page ground (--ground), so the shapes show
10
+ * nowhere else.
11
11
  *
12
12
  * The chrome is lightly translucent and frosted: about 80% of the background
13
13
  * colour (--nxgt-backdrop-chrome-alpha, the app bar) or 87% (--nxgt-backdrop-nav-alpha,
@@ -34,7 +34,7 @@
34
34
  display: none;
35
35
  }
36
36
 
37
- /* The gutter mask without a backdrop (activity-layout.tsx) has the app bar's
37
+ /* The gutter mask without a backdrop (activity-layout.tsx) has the page ground's
38
38
  * translucent fill, which the page's top gutter never had: it fades in over
39
39
  * the first 8px of scroll, so at scroll top nothing changed. A browser
40
40
  * without scroll-driven animations shows it from the start. */
@@ -60,19 +60,6 @@
60
60
  background-image: none;
61
61
  }
62
62
 
63
- /* scroll="document": the frame's chrome fill leaves its top gutter to the
64
- * gutter mask (activity-layout.tsx), a fixed strip with the same fill that
65
- * hides the scrolled page above the sticky app bar. Painting both would
66
- * stack two translucent fills there. */
67
- [data-slot="activity-layout"][data-backdrop][data-scroll="document"]
68
- [data-slot="activity-layout-frame"] {
69
- background:
70
- linear-gradient(
71
- var(--nxgt-backdrop-fill-chrome),
72
- var(--nxgt-backdrop-fill-chrome)
73
- )
74
- bottom / 100% calc(100% - var(--nxgt-backdrop-top)) no-repeat;
75
- }
76
63
  [data-slot="activity-layout"][data-backdrop] > [data-layer] {
77
64
  display: none;
78
65
  }
package/lib/styles.css CHANGED
@@ -52,6 +52,7 @@
52
52
  --color-paper: --alpha(var(--primary) / 0.5%);
53
53
  --color-strix: var(--strix);
54
54
  --color-background: var(--background);
55
+ --color-ground: var(--ground);
55
56
  --color-background-paper: --alpha(var(--primary) / 5%);
56
57
  --color-background-light: --alpha(var(--background) / 30%);
57
58
  --color-foreground: var(--foreground);
@@ -1,12 +1,13 @@
1
1
  /* Theme "ambre", generated.
2
2
  * Seeds: primary oklch(0.52 0.13 48), secondary oklch(0.5 0.09 95), tertiary oklch(0.5 0.14 350), neutral hue 65, tint 0.85.
3
- * Contrast: 61 pairs, 0 failing.
3
+ * Contrast: 65 pairs, 0 failing.
4
4
  * hue-near-status: The primary's hue 48 is 14° from warning (62): they may read alike. Keep an icon and a word with every status.
5
5
  */
6
6
  [data-nxgt-theme="ambre"] {
7
7
  --radius: 0.875rem;
8
8
  --background: oklch(1 0 0);
9
9
  --foreground: oklch(0.227 0.0145 65);
10
+ --ground: color-mix(in oklab, var(--primary) 7%, var(--background));
10
11
  --card: oklch(1 0 0);
11
12
  --card-foreground: oklch(0.227 0.0145 65);
12
13
  --popover: oklch(1 0 0);
@@ -74,6 +75,7 @@
74
75
  [data-nxgt-theme="ambre"] :is(.dark, [data-theme="dark"]):not([data-nxgt-theme="ambre"] [data-nxgt-theme]:not([data-nxgt-theme="ambre"]), [data-nxgt-theme="ambre"] [data-nxgt-theme]:not([data-nxgt-theme="ambre"]) *) {
75
76
  --background: oklch(0.15 0.026 273);
76
77
  --foreground: oklch(0.943 0.0102 65);
78
+ --ground: color-mix(in oklab, var(--primary) 6%, color-mix(in oklab, var(--background) 55%, black));
77
79
  --card: oklch(0.19 0.0161 65);
78
80
  --card-foreground: oklch(0.943 0.0102 65);
79
81
  --popover: oklch(0.19 0.0161 65);
@@ -1,11 +1,12 @@
1
1
  /* Theme "ardoise", generated.
2
2
  * Seeds: primary oklch(0.45 0.065 252), secondary oklch(0.48 0.05 215), tertiary oklch(0.48 0.1 290), neutral hue 250, tint 0.7.
3
- * Contrast: 61 pairs, 0 failing.
3
+ * Contrast: 65 pairs, 0 failing.
4
4
  */
5
5
  [data-nxgt-theme="ardoise"] {
6
6
  --radius: 0.375rem;
7
7
  --background: oklch(1 0 0);
8
8
  --foreground: oklch(0.227 0.0119 250);
9
+ --ground: color-mix(in oklab, var(--primary) 7%, var(--background));
9
10
  --card: oklch(1 0 0);
10
11
  --card-foreground: oklch(0.227 0.0119 250);
11
12
  --popover: oklch(1 0 0);
@@ -73,6 +74,7 @@
73
74
  [data-nxgt-theme="ardoise"] :is(.dark, [data-theme="dark"]):not([data-nxgt-theme="ardoise"] [data-nxgt-theme]:not([data-nxgt-theme="ardoise"]), [data-nxgt-theme="ardoise"] [data-nxgt-theme]:not([data-nxgt-theme="ardoise"]) *) {
74
75
  --background: oklch(0.15 0.026 273);
75
76
  --foreground: oklch(0.943 0.0084 250);
77
+ --ground: color-mix(in oklab, var(--primary) 6%, color-mix(in oklab, var(--background) 55%, black));
76
78
  --card: oklch(0.19 0.0133 250);
77
79
  --card-foreground: oklch(0.943 0.0084 250);
78
80
  --popover: oklch(0.19 0.0133 250);
@@ -1,6 +1,6 @@
1
1
  /* Theme "bleumono", generated.
2
2
  * Seeds: primary oklch(0.46 0.13 255), secondary oklch(0.55 0.06 255), tertiary oklch(0.38 0.17 255), neutral hue 255, tint 1.1.
3
- * Contrast: 61 pairs, 0 failing.
3
+ * Contrast: 65 pairs, 0 failing.
4
4
  * lightness-moved: secondary: lightness 0.550 to 0.520 for AA.
5
5
  * chroma-lowered: tertiary: chroma 0.170 to 0.125 (lowered to fit sRGB or AA).
6
6
  * hue-near-status: The primary's hue 255 is 0° from info (254.624): they may read alike. Keep an icon and a word with every status.
@@ -9,6 +9,7 @@
9
9
  --radius: 0.5rem;
10
10
  --background: oklch(1 0 0);
11
11
  --foreground: oklch(0.227 0.0187 255);
12
+ --ground: color-mix(in oklab, var(--primary) 7%, var(--background));
12
13
  --card: oklch(1 0 0);
13
14
  --card-foreground: oklch(0.227 0.0187 255);
14
15
  --popover: oklch(1 0 0);
@@ -76,6 +77,7 @@
76
77
  [data-nxgt-theme="bleumono"] :is(.dark, [data-theme="dark"]):not([data-nxgt-theme="bleumono"] [data-nxgt-theme]:not([data-nxgt-theme="bleumono"]), [data-nxgt-theme="bleumono"] [data-nxgt-theme]:not([data-nxgt-theme="bleumono"]) *) {
77
78
  --background: oklch(0.15 0.026 273);
78
79
  --foreground: oklch(0.943 0.0132 255);
80
+ --ground: color-mix(in oklab, var(--primary) 6%, color-mix(in oklab, var(--background) 55%, black));
79
81
  --card: oklch(0.19 0.0209 255);
80
82
  --card-foreground: oklch(0.943 0.0132 255);
81
83
  --popover: oklch(0.19 0.0209 255);
@@ -1,6 +1,6 @@
1
1
  /* Theme "bluemoon", generated.
2
2
  * Seeds: primary oklch(0.45 0.12 265), secondary oklch(0.55 0.02 250), tertiary oklch(0.62 0.08 290), neutral hue 265, tint 1.
3
- * Contrast: 61 pairs, 0 failing.
3
+ * Contrast: 65 pairs, 0 failing.
4
4
  * lightness-moved: secondary: lightness 0.550 to 0.520 for AA.
5
5
  * lightness-moved: tertiary: lightness 0.620 to 0.563 for AA.
6
6
  * hue-near-status: The primary's hue 265 is 10° from info (254.624): they may read alike. Keep an icon and a word with every status.
@@ -9,6 +9,7 @@
9
9
  --radius: 0.75rem;
10
10
  --background: oklch(1 0 0);
11
11
  --foreground: oklch(0.227 0.017 265);
12
+ --ground: color-mix(in oklab, var(--primary) 7%, var(--background));
12
13
  --card: oklch(1 0 0);
13
14
  --card-foreground: oklch(0.227 0.017 265);
14
15
  --popover: oklch(1 0 0);
@@ -76,6 +77,7 @@
76
77
  [data-nxgt-theme="bluemoon"] :is(.dark, [data-theme="dark"]):not([data-nxgt-theme="bluemoon"] [data-nxgt-theme]:not([data-nxgt-theme="bluemoon"]), [data-nxgt-theme="bluemoon"] [data-nxgt-theme]:not([data-nxgt-theme="bluemoon"]) *) {
77
78
  --background: oklch(0.15 0.026 273);
78
79
  --foreground: oklch(0.943 0.012 265);
80
+ --ground: color-mix(in oklab, var(--primary) 6%, color-mix(in oklab, var(--background) 55%, black));
79
81
  --card: oklch(0.19 0.019 265);
80
82
  --card-foreground: oklch(0.943 0.012 265);
81
83
  --popover: oklch(0.19 0.019 265);
@@ -1,6 +1,6 @@
1
1
  /* Theme "corail", generated.
2
2
  * Seeds: primary oklch(0.58 0.17 35), secondary oklch(0.52 0.1 70), tertiary oklch(0.5 0.1 200), neutral hue 30, tint 0.8.
3
- * Contrast: 61 pairs, 0 failing.
3
+ * Contrast: 65 pairs, 0 failing.
4
4
  * lightness-moved: primary: lightness 0.580 to 0.575 for AA.
5
5
  * chroma-lowered: tertiary: chroma 0.100 to 0.085 (lowered to fit sRGB or AA).
6
6
  * hue-near-status: The primary's hue 35 is 8° from error (27.325): they may read alike. Keep an icon and a word with every status.
@@ -9,6 +9,7 @@
9
9
  --radius: 1rem;
10
10
  --background: oklch(1 0 0);
11
11
  --foreground: oklch(0.227 0.0136 30);
12
+ --ground: color-mix(in oklab, var(--primary) 7%, var(--background));
12
13
  --card: oklch(1 0 0);
13
14
  --card-foreground: oklch(0.227 0.0136 30);
14
15
  --popover: oklch(1 0 0);
@@ -76,6 +77,7 @@
76
77
  [data-nxgt-theme="corail"] :is(.dark, [data-theme="dark"]):not([data-nxgt-theme="corail"] [data-nxgt-theme]:not([data-nxgt-theme="corail"]), [data-nxgt-theme="corail"] [data-nxgt-theme]:not([data-nxgt-theme="corail"]) *) {
77
78
  --background: oklch(0.15 0.026 273);
78
79
  --foreground: oklch(0.943 0.0096 30);
80
+ --ground: color-mix(in oklab, var(--primary) 6%, color-mix(in oklab, var(--background) 55%, black));
79
81
  --card: oklch(0.19 0.0152 30);
80
82
  --card-foreground: oklch(0.943 0.0096 30);
81
83
  --popover: oklch(0.19 0.0152 30);
@@ -1,11 +1,12 @@
1
1
  /* Theme "foret", generated.
2
2
  * Seeds: primary oklch(0.48 0.1 152), secondary oklch(0.5 0.08 118), tertiary oklch(0.5 0.1 55), neutral hue 140, tint 0.8.
3
- * Contrast: 61 pairs, 0 failing.
3
+ * Contrast: 65 pairs, 0 failing.
4
4
  */
5
5
  [data-nxgt-theme="foret"] {
6
6
  --radius: 0.5rem;
7
7
  --background: oklch(1 0 0);
8
8
  --foreground: oklch(0.227 0.0136 140);
9
+ --ground: color-mix(in oklab, var(--primary) 7%, var(--background));
9
10
  --card: oklch(1 0 0);
10
11
  --card-foreground: oklch(0.227 0.0136 140);
11
12
  --popover: oklch(1 0 0);
@@ -73,6 +74,7 @@
73
74
  [data-nxgt-theme="foret"] :is(.dark, [data-theme="dark"]):not([data-nxgt-theme="foret"] [data-nxgt-theme]:not([data-nxgt-theme="foret"]), [data-nxgt-theme="foret"] [data-nxgt-theme]:not([data-nxgt-theme="foret"]) *) {
74
75
  --background: oklch(0.15 0.026 273);
75
76
  --foreground: oklch(0.943 0.0096 140);
77
+ --ground: color-mix(in oklab, var(--primary) 6%, color-mix(in oklab, var(--background) 55%, black));
76
78
  --card: oklch(0.19 0.0152 140);
77
79
  --card-foreground: oklch(0.943 0.0096 140);
78
80
  --popover: oklch(0.19 0.0152 140);
@@ -1,11 +1,12 @@
1
1
  /* Theme "lavande", generated.
2
2
  * Seeds: primary oklch(46% 0.1135 276.35), secondary oklch(48% 0.1135 245.65), tertiary oklch(45.91% 0.1864 310.75), neutral hue 292, tint 1.
3
- * Contrast: 61 pairs, 0 failing.
3
+ * Contrast: 65 pairs, 0 failing.
4
4
  */
5
5
  [data-nxgt-theme="lavande"] {
6
6
  --radius: 0.625rem;
7
7
  --background: oklch(1 0 0);
8
8
  --foreground: oklch(0.227 0.017 290);
9
+ --ground: color-mix(in oklab, var(--primary) 7%, var(--background));
9
10
  --card: oklch(1 0 0);
10
11
  --card-foreground: oklch(0.227 0.017 290);
11
12
  --popover: oklch(1 0 0);
@@ -73,6 +74,7 @@
73
74
  [data-nxgt-theme="lavande"] :is(.dark, [data-theme="dark"]):not([data-nxgt-theme="lavande"] [data-nxgt-theme]:not([data-nxgt-theme="lavande"]), [data-nxgt-theme="lavande"] [data-nxgt-theme]:not([data-nxgt-theme="lavande"]) *) {
74
75
  --background: oklch(0.15 0.026 273);
75
76
  --foreground: oklch(0.943 0.012 291.3);
77
+ --ground: color-mix(in oklab, var(--primary) 6%, color-mix(in oklab, var(--background) 55%, black));
76
78
  --card: oklch(0.19 0.019 289.4);
77
79
  --card-foreground: oklch(0.943 0.012 291.3);
78
80
  --popover: oklch(0.19 0.019 289.4);
@@ -1,6 +1,6 @@
1
1
  /* Theme "menthe", generated.
2
2
  * Seeds: primary oklch(0.5 0.105 168), secondary oklch(0.5 0.09 205), tertiary oklch(0.52 0.11 120), neutral hue 170, tint 0.8.
3
- * Contrast: 61 pairs, 0 failing.
3
+ * Contrast: 65 pairs, 0 failing.
4
4
  * chroma-lowered: primary: chroma 0.105 to 0.101 (lowered to fit sRGB or AA).
5
5
  * chroma-lowered: secondary: chroma 0.090 to 0.085 (lowered to fit sRGB or AA).
6
6
  * hue-near-status: The primary's hue 168 is 17° from success (184.704): they may read alike. Keep an icon and a word with every status.
@@ -9,6 +9,7 @@
9
9
  --radius: 1rem;
10
10
  --background: oklch(1 0 0);
11
11
  --foreground: oklch(0.227 0.0136 170);
12
+ --ground: color-mix(in oklab, var(--primary) 7%, var(--background));
12
13
  --card: oklch(1 0 0);
13
14
  --card-foreground: oklch(0.227 0.0136 170);
14
15
  --popover: oklch(1 0 0);
@@ -76,6 +77,7 @@
76
77
  [data-nxgt-theme="menthe"] :is(.dark, [data-theme="dark"]):not([data-nxgt-theme="menthe"] [data-nxgt-theme]:not([data-nxgt-theme="menthe"]), [data-nxgt-theme="menthe"] [data-nxgt-theme]:not([data-nxgt-theme="menthe"]) *) {
77
78
  --background: oklch(0.15 0.026 273);
78
79
  --foreground: oklch(0.943 0.0096 170);
80
+ --ground: color-mix(in oklab, var(--primary) 6%, color-mix(in oklab, var(--background) 55%, black));
79
81
  --card: oklch(0.19 0.0152 170);
80
82
  --card-foreground: oklch(0.943 0.0096 170);
81
83
  --popover: oklch(0.19 0.0152 170);
@@ -1,6 +1,6 @@
1
1
  /* Theme "ocean", generated.
2
2
  * Seeds: primary oklch(0.5 0.12 228), secondary oklch(0.52 0.09 192), tertiary oklch(0.5 0.13 268), neutral hue 230, tint 0.9.
3
- * Contrast: 61 pairs, 0 failing.
3
+ * Contrast: 65 pairs, 0 failing.
4
4
  * chroma-lowered: primary: chroma 0.120 to 0.098 (lowered to fit sRGB or AA).
5
5
  * lightness-moved: secondary: lightness 0.520 to 0.505 for AA.
6
6
  * chroma-lowered: secondary: chroma 0.090 to 0.087 (lowered to fit sRGB or AA).
@@ -9,6 +9,7 @@
9
9
  --radius: 0.75rem;
10
10
  --background: oklch(1 0 0);
11
11
  --foreground: oklch(0.227 0.0153 230);
12
+ --ground: color-mix(in oklab, var(--primary) 7%, var(--background));
12
13
  --card: oklch(1 0 0);
13
14
  --card-foreground: oklch(0.227 0.0153 230);
14
15
  --popover: oklch(1 0 0);
@@ -76,6 +77,7 @@
76
77
  [data-nxgt-theme="ocean"] :is(.dark, [data-theme="dark"]):not([data-nxgt-theme="ocean"] [data-nxgt-theme]:not([data-nxgt-theme="ocean"]), [data-nxgt-theme="ocean"] [data-nxgt-theme]:not([data-nxgt-theme="ocean"]) *) {
77
78
  --background: oklch(0.15 0.026 273);
78
79
  --foreground: oklch(0.943 0.0108 230);
80
+ --ground: color-mix(in oklab, var(--primary) 6%, color-mix(in oklab, var(--background) 55%, black));
79
81
  --card: oklch(0.19 0.0171 230);
80
82
  --card-foreground: oklch(0.943 0.0108 230);
81
83
  --popover: oklch(0.19 0.0171 230);
@@ -1,12 +1,13 @@
1
1
  /* Theme "pink", generated.
2
2
  * Seeds: primary oklch(0.55 0.2 345), secondary oklch(0.5 0.14 320), tertiary oklch(0.5 0.1 200), neutral hue 345, tint 0.7.
3
- * Contrast: 61 pairs, 0 failing.
3
+ * Contrast: 65 pairs, 0 failing.
4
4
  * chroma-lowered: tertiary: chroma 0.100 to 0.085 (lowered to fit sRGB or AA).
5
5
  */
6
6
  [data-nxgt-theme="pink"] {
7
7
  --radius: 1.25rem;
8
8
  --background: oklch(1 0 0);
9
9
  --foreground: oklch(0.227 0.0119 345);
10
+ --ground: color-mix(in oklab, var(--primary) 7%, var(--background));
10
11
  --card: oklch(1 0 0);
11
12
  --card-foreground: oklch(0.227 0.0119 345);
12
13
  --popover: oklch(1 0 0);
@@ -74,6 +75,7 @@
74
75
  [data-nxgt-theme="pink"] :is(.dark, [data-theme="dark"]):not([data-nxgt-theme="pink"] [data-nxgt-theme]:not([data-nxgt-theme="pink"]), [data-nxgt-theme="pink"] [data-nxgt-theme]:not([data-nxgt-theme="pink"]) *) {
75
76
  --background: oklch(0.15 0.026 273);
76
77
  --foreground: oklch(0.943 0.0084 345);
78
+ --ground: color-mix(in oklab, var(--primary) 6%, color-mix(in oklab, var(--background) 55%, black));
77
79
  --card: oklch(0.19 0.0133 345);
78
80
  --card-foreground: oklch(0.943 0.0084 345);
79
81
  --popover: oklch(0.19 0.0133 345);