@dxos/ui-theme 0.9.0 → 0.9.1-staging.ee54ba693a

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 (36) hide show
  1. package/dist/lib/browser/index.mjs +37 -7
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +37 -7
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/plugin/node-cjs/main.css +56 -27
  8. package/dist/plugin/node-cjs/main.css.map +2 -2
  9. package/dist/plugin/node-cjs/meta.json +1 -1
  10. package/dist/plugin/node-esm/main.css +56 -27
  11. package/dist/plugin/node-esm/main.css.map +2 -2
  12. package/dist/plugin/node-esm/meta.json +1 -1
  13. package/dist/types/src/util/index.d.ts +2 -0
  14. package/dist/types/src/util/index.d.ts.map +1 -1
  15. package/dist/types/src/util/mx.d.ts.map +1 -1
  16. package/dist/types/src/util/tv.d.ts +27 -0
  17. package/dist/types/src/util/tv.d.ts.map +1 -0
  18. package/dist/types/src/util/tv.test.d.ts +2 -0
  19. package/dist/types/src/util/tv.test.d.ts.map +1 -0
  20. package/dist/types/src/util/tw-merge-config.d.ts +17 -0
  21. package/dist/types/src/util/tw-merge-config.d.ts.map +1 -0
  22. package/dist/types/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +8 -7
  24. package/src/Theme.stories.tsx +1 -1
  25. package/src/css/components/button.css +10 -9
  26. package/src/css/components/state.css +2 -25
  27. package/src/css/components/surface.css +65 -12
  28. package/src/css/components/tag.css +2 -2
  29. package/src/css/layout/size.css +1 -1
  30. package/src/css/theme/semantic.css +162 -75
  31. package/src/css/theme/spacing.css +7 -5
  32. package/src/util/index.ts +2 -0
  33. package/src/util/mx.ts +5 -35
  34. package/src/util/tv.test.ts +38 -0
  35. package/src/util/tv.ts +45 -0
  36. package/src/util/tw-merge-config.ts +46 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/ui-theme",
3
- "version": "0.9.0",
3
+ "version": "0.9.1-staging.ee54ba693a",
4
4
  "description": "A set of design system tokens to use with DXOS UI.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -48,21 +48,22 @@
48
48
  "postcss-nesting": "^13.0.1",
49
49
  "tailwind-merge": "^3.5.0",
50
50
  "tailwind-scrollbar": "^4.0.0",
51
+ "tailwind-variants": "^3.2.2",
51
52
  "tailwindcss": "^4.3.0",
52
53
  "tailwindcss-radix": "^4.0.2",
53
- "@dxos/log": "0.9.0",
54
- "@dxos/util": "0.9.0",
55
- "@dxos/node-std": "0.9.0"
54
+ "@dxos/log": "0.9.1-staging.ee54ba693a",
55
+ "@dxos/node-std": "0.9.1-staging.ee54ba693a",
56
+ "@dxos/util": "0.9.1-staging.ee54ba693a"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@types/lodash.merge": "^4.6.6",
59
60
  "@types/postcss-import": "^14.0.3",
60
- "esbuild": "0.28.0",
61
+ "esbuild": "0.28.1",
61
62
  "vite": "^8.0.16",
62
- "@dxos/ui-types": "0.9.0"
63
+ "@dxos/ui-types": "0.9.1-staging.ee54ba693a"
63
64
  },
64
65
  "peerDependencies": {
65
- "react": "~19.2.3"
66
+ "react": "~19.2.7"
66
67
  },
67
68
  "publishConfig": {
68
69
  "access": "public"
@@ -5,7 +5,7 @@
5
5
  import { type Meta } from '@storybook/react-vite';
6
6
  import React, { useLayoutEffect, useRef, useState } from 'react';
7
7
 
8
- import { hueShades, hues } from './defs';
8
+ import { hues, hueShades } from './defs';
9
9
  import { mx } from './util';
10
10
 
11
11
  // prettier-ignore
@@ -18,9 +18,15 @@
18
18
  color: var(--dx-valence-text, inherit);
19
19
  background: var(--dx-valence-bg, var(--color-input-bg));
20
20
  }
21
+ /* Toggle / tab group: the selected item reads as standard text on a surface-relative fill (selected-surface
22
+ * tracks the enclosing --surface-bg, so it stays visible on any surface); unselected items recede to subdued. */
21
23
  &[aria-pressed='true'],
22
24
  &[aria-checked='true'] {
23
- @apply text-accent-text bg-attention-surface;
25
+ @apply text-base-fg bg-selected-surface;
26
+ }
27
+ &[aria-pressed='false'],
28
+ &[aria-checked='false'] {
29
+ @apply text-description;
24
30
  }
25
31
 
26
32
  /* Disabled styles */
@@ -32,14 +38,9 @@
32
38
  &[data-variant='ghost'] {
33
39
  @apply bg-transparent;
34
40
  &:hover {
35
- /* Tint relative to the host surface (--surface-bg, set by the enclosing dx-*-surface zone)
36
- * so the hover reads correctly on any background: darker in light mode, lighter in dark.
37
- * Falls back to base-surface when no surface zone is in scope. */
38
- background: light-dark(
39
- oklch(from var(--surface-bg, var(--color-base-surface)) calc(l - 0.05) c h),
40
- oklch(from var(--surface-bg, var(--color-base-surface)) calc(l + 0.05) c h)
41
- );
42
- @apply text-inherit;
41
+ /* Shares the surface-derived control hover (--color-hover-surface tracks the enclosing
42
+ * --surface-bg) so every variant tints consistently on any background. */
43
+ @apply bg-hover-surface text-inherit;
43
44
  }
44
45
  &[data-state='open'] {
45
46
  @apply bg-input-bg;
@@ -19,12 +19,12 @@
19
19
  */
20
20
  .dx-hover {
21
21
  @apply cursor-pointer
22
- hover:bg-hover-surface! hover:text-hover-fg!
22
+ hover:bg-hover-surface-subtle! hover:text-hover-fg!
23
23
  hover:aria-selected:bg-selected-surface-hover hover:aria-selected:text-selected-fg
24
24
  hover:aria-[current=true]:bg-current-surface-hover hover:aria-[current=true]:text-current-fg;
25
25
  }
26
26
  .dx-hover-row {
27
- @apply group-hover/row:bg-hover-surface! group-hover/row:text-hover-fg!
27
+ @apply group-hover/row:bg-hover-surface-subtle! group-hover/row:text-hover-fg!
28
28
  group-hover/row:group-aria-selected/row:bg-selected-surface-hover!
29
29
  group-hover/row:group-aria-selected/row:text-selected-fg!
30
30
  group-hover/row:group-aria-[current=true]/row:bg-current-surface-hover!
@@ -74,26 +74,3 @@
74
74
  hover:data-[highlighted]:bg-current-surface-hover;
75
75
  }
76
76
  }
77
-
78
- @layer dx-tokens {
79
- /*
80
- * Sidebars sit on a different surface than the main content, so nudge the state highlights to
81
- * keep contrast: offset each token off its `*-base` (see semantic.css) — darker in light mode,
82
- * lighter in dark. Tune the magnitude via --dx-sidebar-l-shift.
83
- */
84
- .dx-main-sidebar {
85
- --dx-sidebar-l-shift: 0.04;
86
- --color-hover-surface: light-dark(
87
- oklch(from var(--dx-hover-surface-base) calc(l - var(--dx-sidebar-l-shift)) c h),
88
- oklch(from var(--dx-hover-surface-base) calc(l + var(--dx-sidebar-l-shift)) c h)
89
- );
90
- --color-current-surface: light-dark(
91
- oklch(from var(--dx-current-surface-base) calc(l - var(--dx-sidebar-l-shift)) c h),
92
- oklch(from var(--dx-current-surface-base) calc(l + var(--dx-sidebar-l-shift)) c h)
93
- );
94
- --color-current-surface-hover: light-dark(
95
- oklch(from var(--dx-current-surface-hover-base) calc(l - var(--dx-sidebar-l-shift)) c h),
96
- oklch(from var(--dx-current-surface-hover-base) calc(l + var(--dx-sidebar-l-shift)) c h)
97
- );
98
- }
99
- }
@@ -1,5 +1,13 @@
1
1
  /**
2
2
  * Surfaces
3
+ *
4
+ * A surface zone paints a background AND publishes its colour as `--surface-bg`, so descendant
5
+ * controls can derive a visible hover/current highlight from the actual surface (see the
6
+ * derivation block below) and surface-matching elements (avatars, gradients) can read it directly.
7
+ *
8
+ * Always enter a surface via one of these `dx-*-surface` classes. A bare `bg-*-surface` utility
9
+ * paints the colour but does NOT publish `--surface-bg`, so control hover collapses onto the
10
+ * base-surface fallback and can become invisible when the surface shares that elevation.
3
11
  */
4
12
 
5
13
  @layer dx-components {
@@ -8,42 +16,87 @@
8
16
  --surface-bg: var(--color-base-surface);
9
17
  }
10
18
 
19
+ .dx-deck-surface {
20
+ @apply bg-deck-surface text-base-fg;
21
+ --surface-bg: var(--color-deck-surface);
22
+ }
23
+
11
24
  .dx-sidebar-surface {
12
25
  @apply bg-sidebar-surface text-base-fg;
13
26
  --surface-bg: var(--color-sidebar-surface);
14
27
  }
15
28
 
29
+ .dx-header-surface {
30
+ @apply bg-header-surface text-base-fg;
31
+ --surface-bg: var(--color-header-surface);
32
+ }
33
+
34
+ .dx-card-surface {
35
+ @apply bg-card-surface text-base-fg;
36
+ --surface-bg: var(--color-card-surface);
37
+ }
38
+
39
+ .dx-group-surface {
40
+ @apply bg-group-surface text-base-fg;
41
+ --surface-bg: var(--color-group-surface);
42
+ }
43
+
44
+ .dx-input-surface {
45
+ @apply bg-input-surface text-base-fg;
46
+ --surface-bg: var(--color-input-surface);
47
+ }
48
+
49
+ .dx-toolbar-surface {
50
+ @apply bg-toolbar-surface text-base-fg;
51
+ --surface-bg: var(--color-toolbar-surface);
52
+ }
53
+
16
54
  .dx-modal-surface {
17
55
  @apply bg-modal-surface text-base-fg backdrop-blur-md;
18
56
  --surface-bg: var(--color-modal-surface);
19
57
  }
20
58
 
21
- .dx-attention-surface {
22
- @apply bg-attention-surface text-base-fg;
23
- --surface-bg: var(--color-attention-surface);
24
- }
25
-
26
59
  .dx-popover-surface {
27
60
  @apply bg-popover-surface text-base-fg backdrop-blur-md;
28
61
  --surface-bg: var(--color-popover-surface);
29
62
  }
63
+
64
+ .dx-attention-surface {
65
+ @apply bg-attention-surface text-base-fg;
66
+ --surface-bg: var(--color-attention-surface);
67
+ }
30
68
  }
31
69
 
32
70
  @layer dx-tokens {
33
71
  /*
34
- * Elevated surfaces (modal, popover) sit higher than the hover/current base tokens, so the
35
- * un-offset state colours collide with the surface itself (e.g. in dark mode the popover surface
36
- * and hover surface are both neutral-750 the highlight is invisible). Re-derive the state
37
- * colours off the actual surface (--surface-bg) via relative oklch darker in light mode,
38
- * lighter in dark so highlights stay visible regardless of how the surface tokens are tuned.
39
- * Mirrors the .dx-main-sidebar offset in state.css.
72
+ * Re-derive the state colours (hover, current, selected) off the host surface for every context
73
+ * that publishes its own --surface-bg. Custom-property substitution runs once per scope, so the
74
+ * formula must be repeated here (it cannot simply inherit from semantic.css, which resolved it
75
+ * against the root surface). Listing all surface zones in one block keeps the offsets identical
76
+ * across surfacesthere is no per-surface divergence to tune.
77
+ *
78
+ * Magnitudes mirror semantic.css; selected-* track current-* via the aliases declared there.
40
79
  */
80
+ .dx-base-surface,
81
+ .dx-deck-surface,
82
+ .dx-sidebar-surface,
83
+ .dx-header-surface,
84
+ .dx-card-surface,
85
+ .dx-group-surface,
86
+ .dx-input-surface,
87
+ .dx-toolbar-surface,
41
88
  .dx-modal-surface,
42
- .dx-popover-surface {
89
+ .dx-popover-surface,
90
+ .dx-attention-surface,
91
+ .dx-main-sidebar {
43
92
  --color-hover-surface: light-dark(
44
93
  oklch(from var(--surface-bg) calc(l - 0.08) c h),
45
94
  oklch(from var(--surface-bg) calc(l + 0.08) c h)
46
95
  );
96
+ --color-hover-surface-subtle: light-dark(
97
+ oklch(from var(--surface-bg) calc(l - 0.02) c h),
98
+ oklch(from var(--surface-bg) calc(l + 0.02) c h)
99
+ );
47
100
  --color-current-surface: light-dark(
48
101
  oklch(from var(--surface-bg) calc(l - 0.1) c h),
49
102
  oklch(from var(--surface-bg) calc(l + 0.1) c h)
@@ -11,9 +11,9 @@
11
11
 
12
12
  .dx-tag--anchor {
13
13
  /* NOTE: Margin offsets padding so that border is flush with text. This should only apply to anchor tags inside of Codemirror. */
14
- @apply inline cursor-pointer px-[4px] py-0.5;
14
+ @apply inline-block cursor-pointer px-[4px] py-0.5;
15
15
  /** TODO(burdon): Configure secondary color. */
16
- @apply bg-input-surface ring ring-neutral-border rounded-sm;
16
+ @apply bg-input-surface ring-inset ring ring-neutral-border rounded-sm;
17
17
  &:hover {
18
18
  @apply bg-hover-surface;
19
19
  }
@@ -64,7 +64,7 @@
64
64
  }
65
65
 
66
66
  .dx-card-popover {
67
- @apply inline-card-max-width bg-card-surface;
67
+ @apply inline-card-max-width bg-popover-surface;
68
68
  max-width: min(var(--radix-popper-available-width), var(--spacing-card-max-width));
69
69
  max-height: min(var(--radix-popper-available-height), var(--spacing-card-max-height));
70
70
  }
@@ -13,95 +13,180 @@
13
13
  * Elevation ladder — strictly monotonic, z-order low → high.
14
14
  * Dark: darker = lower. Light: lighter = higher (inverted toward white).
15
15
  *
16
- * Level Name Roles
17
- * 0 void window gaps, scrim base
18
- * 1 rail L0 icon rail
19
- * 2 chrome L1/R sidebars, header
20
- * 3 canvas base, deck
21
- * 4 raised card, group, input
22
- * 5 bar toolbar (sticky, drop-shadowed)
23
- * 6 modal dialog, modal, popover
24
- * 7 float menu, toast, tooltip
16
+ * Level Name Roles
17
+ * 0 void deck, scrim base
18
+ * 1 base base content, L0 icon rail
19
+ * 2 chrome L1/R sidebars, header
20
+ * 3 card card
21
+ * 4 raised group, input, focus/attention
22
+ * 5 bar dialog/modal, toolbar (sticky, drop-shadowed)
23
+ * 6 modal popover, menu, toast, tooltip
24
+ * 7 float
25
25
  *
26
26
  * These are private (--dx-*) knobs — use named surface tokens in components, not these directly.
27
27
  */
28
28
  --dx-elevation-0: light-dark(var(--color-neutral-300), var(--color-neutral-950));
29
- --dx-elevation-1: light-dark(var(--color-neutral-200), var(--color-neutral-900));
30
- --dx-elevation-2: light-dark(var(--color-neutral-125), var(--color-neutral-875));
31
- --dx-elevation-3: light-dark(var(--color-neutral-125), var(--color-neutral-850));
32
- --dx-elevation-4: light-dark(var(--color-neutral-100), var(--color-neutral-825));
33
- --dx-elevation-5: light-dark(var(--color-neutral-75), var(--color-neutral-800));
34
- --dx-elevation-6: light-dark(var(--color-neutral-50), var(--color-neutral-775));
35
- --dx-elevation-7: light-dark(white, var(--color-neutral-750));
36
-
37
- /* Surfaces — each maps to exactly one elevation level */
38
- --color-sidebar-surface: var(--dx-elevation-2);
39
- --color-header-surface: var(--dx-elevation-2);
40
- --color-deck-surface: var(--dx-elevation-3);
41
- --color-base-surface: var(--dx-elevation-3);
42
- --color-card-surface: var(--dx-elevation-3);
43
- --color-group-surface: var(--dx-elevation-4);
44
- /* Subtle alternate of group-surface for zebra striping (e.g. calendar months) — offset off
45
- * elevation-4 via relative oklch so it tracks the ramp: darker in light mode, lighter in dark. */
46
- --color-group-alt-surface: light-dark(
47
- oklch(from var(--dx-elevation-4) calc(l - 0.03) c h),
48
- oklch(from var(--dx-elevation-4) calc(l + 0.03) c h)
49
- );
50
- --color-input-surface: var(--dx-elevation-4);
51
- --color-toolbar-surface: var(--dx-elevation-5);
52
- --color-modal-surface: var(--dx-elevation-6);
53
- --color-popover-surface: var(--dx-elevation-6);
29
+ --dx-elevation-1: light-dark(var(--color-neutral-300), var(--color-neutral-925));
30
+ --dx-elevation-2: light-dark(var(--color-neutral-200), var(--color-neutral-900));
31
+ --dx-elevation-3: light-dark(var(--color-neutral-125), var(--color-neutral-875));
32
+ --dx-elevation-4: light-dark(var(--color-neutral-125), var(--color-neutral-850));
33
+ --dx-elevation-5: light-dark(var(--color-neutral-100), var(--color-neutral-825));
34
+ --dx-elevation-6: light-dark(var(--color-neutral-75), var(--color-neutral-800));
35
+ --dx-elevation-7: light-dark(var(--color-neutral-50), var(--color-neutral-775));
36
+ --dx-elevation-8: light-dark(white, var(--color-neutral-750));
37
+
38
+ /**
39
+ * Elevation 0
40
+ */
54
41
 
55
- /* Sidebar / panel layout levels */
56
- --color-l0-surface: var(--dx-elevation-1);
57
- --color-l1-surface: var(--dx-elevation-2);
58
- --color-r0-surface: var(--dx-elevation-2);
59
- --color-r1-surface: var(--dx-elevation-2);
42
+ --color-deck-surface: var(--dx-elevation-0);
60
43
 
44
+ /* Translucent dimming layer; tracks the lowest elevation (void) rather than a raw neutral. */
61
45
  --color-scrim-surface: light-dark(
62
- oklch(from var(--color-neutral-50) l c h / 0.5),
63
- oklch(from var(--color-neutral-950) l c h / 0.25)
46
+ oklch(from var(--dx-elevation-0) l c h / 0.5),
47
+ oklch(from var(--dx-elevation-0) l c h / 0.25)
64
48
  );
65
49
 
66
- --color-inverse-surface: light-dark(var(--color-neutral-800), var(--color-neutral-200));
67
- --color-inverse-fg: light-dark(var(--color-neutral-50), var(--color-neutral-950));
50
+ /**
51
+ * Elevation 1
52
+ */
68
53
 
54
+ --color-card-surface: var(--dx-elevation-1);
55
+
56
+ /**
57
+ * Elevation 2
58
+ */
59
+
60
+ --color-base-surface: var(--dx-elevation-2);
69
61
  --color-base-fg: light-dark(var(--color-neutral-950), var(--color-neutral-150));
70
62
 
63
+ /* Map attention to focus-surface logic so that elements acting as active attention zones highlight correctly on focus. */
64
+ --color-attention-surface: var(--dx-elevation-2);
65
+ --color-attention-contains: oklch(from var(--color-accent-bg) l c h / 0.3);
66
+
67
+ /**
68
+ * Elevation 3
69
+ */
70
+
71
+ --color-header-surface: var(--dx-elevation-3);
72
+
73
+ --color-sidebar-surface: var(--dx-elevation-3);
71
74
  /* Sidebar body text. Default sits one ramp stop below the current/selected emphasis. */
72
75
  --color-sidebar-fg: light-dark(var(--color-neutral-700), var(--color-neutral-250));
73
76
  --color-sidebar-current-fg: light-dark(var(--color-neutral-950), var(--color-neutral-75));
74
77
 
75
- /* Focus */
76
- --color-focus-surface: light-dark(var(--color-neutral-50), var(--color-neutral-850));
78
+ /* Sidebar / panel layout levels */
79
+ --color-l0-surface: var(--dx-elevation-2);
80
+ --color-l1-surface: var(--dx-elevation-3);
81
+ --color-r0-surface: var(--dx-elevation-3);
82
+ --color-r1-surface: var(--dx-elevation-3);
83
+
84
+ /**
85
+ * Elevation 4
86
+ */
87
+
88
+ --color-toolbar-surface: var(--dx-elevation-4);
89
+
90
+ /**
91
+ * Elevation 5
92
+ */
93
+
94
+ /* Focus — a raised content surface (one level above card) so an attended pane reads as lifted. */
95
+ --color-focus-surface: var(--dx-elevation-5);
77
96
  --color-focus-ring: light-dark(var(--color-blue-400), var(--color-blue-600));
78
97
  --color-focus-ring-subtle: light-dark(var(--color-neutral-300), var(--color-neutral-600));
79
98
 
80
- /* Map attention to focus-surface logic so that elements acting as active attention zones highlight correctly on focus. */
81
- --color-attention-surface: var(--color-focus-surface);
82
- --color-attention-contains: oklch(from var(--color-accent-bg) l c h / 0.3);
99
+ --color-group-surface: var(--dx-elevation-5);
100
+ /* Subtle alternate of group-surface for zebra striping (e.g. calendar months) — offset off
101
+ * elevation-4 via relative oklch so it tracks the ramp: darker in light mode, lighter in dark. */
102
+ --color-group-alt-surface: light-dark(
103
+ oklch(from var(--dx-elevation-5) calc(l - 0.03) c h),
104
+ oklch(from var(--dx-elevation-5) calc(l + 0.03) c h)
105
+ );
83
106
 
84
- /* input */
107
+ --color-input-surface: var(--dx-elevation-5);
108
+
109
+ /**
110
+ * Elevation 6
111
+ */
112
+
113
+ --color-modal-surface: var(--dx-elevation-6);
114
+
115
+ /**
116
+ * Elevation 7
117
+ */
118
+
119
+ --color-popover-surface: var(--dx-elevation-7);
120
+
121
+ /**
122
+ * Elevation 8
123
+ */
124
+
125
+ /**
126
+ * Misc
127
+ */
128
+
129
+ --color-inverse-surface: light-dark(var(--color-neutral-800), var(--color-neutral-200));
130
+ --color-inverse-fg: light-dark(var(--color-neutral-50), var(--color-neutral-950));
131
+
132
+ /*
133
+ * Text emphasis — foregrounds at decreasing emphasis, anchored on --color-base-fg (strongest):
134
+ * base-fg → description → subdued → placeholder (weakest).
135
+ * Tuned for legibility against the surfaces rather than mapped to the elevation ladder (which
136
+ * governs backgrounds, not text contrast), so these stay explicit neutrals.
137
+ */
138
+ --color-description: light-dark(var(--color-neutral-600), var(--color-neutral-400));
139
+ --color-subdued: light-dark(var(--color-neutral-500), var(--color-neutral-600));
140
+ --color-placeholder: light-dark(var(--color-neutral-400), var(--color-neutral-600));
141
+
142
+ /*
143
+ * Controls — the input/control fill is a deliberate off-ladder token (cf. the grid "paper"
144
+ * surfaces). It backs text inputs and the default/ghost button (bg-input-bg) and must contrast
145
+ * whatever surface it sits on: a subtle inset well in light mode, a raised fill in dark mode that
146
+ * reads lighter than the topmost surface. Do not map it to an elevation level.
147
+ */
85
148
  --color-input-bg: light-dark(var(--color-neutral-250), var(--color-neutral-700));
86
- --color-input-fg: light-dark(var(--color-neutral-950), var(--color-neutral-150));
149
+ --color-input-fg: var(--color-base-fg);
87
150
 
88
- /*
89
- * hover (:hover, data-highlighted)
90
- * `*-base` primitives are the un-offset values;
91
- * surface contexts (e.g. .dx-main-sidebar in state.css) re-derive the public token off the base via relative oklch.
151
+ /*
152
+ * State surfaces (hover, current, selected) are derived from the host surface (--surface-bg) via
153
+ * relative oklch darker in light mode, lighter in dark — so a highlight stays visible at any
154
+ * elevation. The values here resolve against the root --surface-bg (= base-surface, set in
155
+ * base.css), giving the correct default for content outside any surface zone. Surface zones
156
+ * publish their own --surface-bg and re-derive these tokens in surface.css, which is where the
157
+ * substitution re-runs for that subtree.
158
+ *
159
+ * Offset magnitudes (lightness): hover ∓0.08, subtle ∓0.02, current ∓0.10, current-hover ∓0.12.
160
+ * Keep these in sync with the per-zone re-derivation in components/surface.css.
92
161
  */
93
- --dx-hover-surface-base: light-dark(var(--color-neutral-200), var(--color-neutral-850));
94
- --color-hover-surface: var(--dx-hover-surface-base);
162
+ --color-hover-surface: light-dark(
163
+ oklch(from var(--surface-bg, var(--color-base-surface)) calc(l - 0.08) c h),
164
+ oklch(from var(--surface-bg, var(--color-base-surface)) calc(l + 0.08) c h)
165
+ );
95
166
  --color-hover-fg: light-dark(var(--color-neutral-950), var(--color-neutral-150));
96
167
 
168
+ /*
169
+ * Subtle region hover for large highlight targets (list rows, tiles) where the full
170
+ * control-strength hover reads as too heavy. Consumed by `.dx-hover` / `.dx-hover-row`;
171
+ * controls (buttons) keep --color-hover-surface.
172
+ */
173
+ --color-hover-surface-subtle: light-dark(
174
+ oklch(from var(--surface-bg, var(--color-base-surface)) calc(l - 0.02) c h),
175
+ oklch(from var(--surface-bg, var(--color-base-surface)) calc(l + 0.02) c h)
176
+ );
177
+
97
178
  /* current (aria-current) */
98
- --dx-current-surface-base: light-dark(var(--color-neutral-150), var(--color-neutral-800));
99
- --dx-current-surface-hover-base: light-dark(var(--color-neutral-150), var(--color-neutral-600));
100
- --color-current-surface: var(--dx-current-surface-base);
101
- --color-current-surface-hover: var(--dx-current-surface-hover-base);
179
+ --color-current-surface: light-dark(
180
+ oklch(from var(--surface-bg, var(--color-base-surface)) calc(l - 0.1) c h),
181
+ oklch(from var(--surface-bg, var(--color-base-surface)) calc(l + 0.1) c h)
182
+ );
183
+ --color-current-surface-hover: light-dark(
184
+ oklch(from var(--surface-bg, var(--color-base-surface)) calc(l - 0.12) c h),
185
+ oklch(from var(--surface-bg, var(--color-base-surface)) calc(l + 0.12) c h)
186
+ );
102
187
  --color-current-fg: light-dark(var(--color-neutral-950), var(--color-neutral-50));
103
188
 
104
- /* selected (aria-selected) */
189
+ /* selected (aria-selected) tracks current */
105
190
  --color-selected-surface: var(--color-current-surface);
106
191
  --color-selected-surface-hover: var(--color-current-surface-hover);
107
192
  --color-selected-fg: var(--color-current-fg);
@@ -113,13 +198,15 @@
113
198
  --color-accent-text: light-dark(var(--color-blue-600), var(--color-blue-400));
114
199
  --color-accent-text-hover: light-dark(var(--color-blue-500), var(--color-blue-500));
115
200
 
201
+ /* Unaccented */
116
202
  --color-un-accent: var(--color-neutral-400);
117
203
  --color-un-accent-hover: var(--color-neutral-500);
118
204
 
119
205
  /* Separator */
120
- --color-primary-separator: light-dark(var(--color-neutral-300), var(--color-neutral-700));
206
+ --color-primary-separator: light-dark(var(--color-neutral-200), var(--color-neutral-800));
121
207
  --color-separator: light-dark(var(--color-neutral-200), var(--color-neutral-700));
122
- --color-subdued-separator: light-dark(var(--color-neutral-250), var(--color-neutral-750));
208
+ --color-input-separator: light-dark(var(--color-neutral-250), var(--color-neutral-750));
209
+ --color-subdued-separator: light-dark(var(--color-neutral-250), var(--color-neutral-775));
123
210
  --color-active-separator: light-dark(var(--color-blue-500), var(--color-blue-500));
124
211
 
125
212
  /* Scrollbar */
@@ -128,16 +215,21 @@
128
215
  --color-scrollbar-thumb-active: light-dark(var(--color-neutral-200), var(--color-neutral-600));
129
216
  --color-scrollbar-thumb-hover: light-dark(var(--color-neutral-200), var(--color-neutral-600));
130
217
 
131
- /* Sheet */
132
- --color-axis-selected-surface: light-dark(var(--color-neutral-100), var(--color-neutral-900));
133
- --color-axis-selected-text: light-dark(var(--color-neutral-100), var(--color-neutral-900));
134
- --color-axis-surface: light-dark(var(--color-neutral-50), var(--color-neutral-800));
135
- --color-axis-text: light-dark(var(--color-neutral-800), var(--color-neutral-200));
136
-
137
218
  /*
138
- * Grid
219
+ * Sheet / Grid — intentionally OFF the elevation ladder.
139
220
  * NOTE: Update main.css when changing grid tokens (and restart dev server).
221
+ *
222
+ * These are "paper" surfaces for the data grid: maximum-contrast canvases (near-white in light,
223
+ * near-black in dark) plus saturated selection/comment accents. The elevation ladder is
224
+ * monotonic (light: lighter = higher), so it cannot express a surface that is simultaneously the
225
+ * lightest in light mode and the darkest in dark mode. Keep these as explicit neutrals/accents;
226
+ * do not fold them into --dx-elevation-*.
140
227
  */
228
+ --color-axis-surface: light-dark(var(--color-neutral-100), var(--color-neutral-900));
229
+ --color-axis-text: light-dark(var(--color-neutral-700), var(--color-neutral-300));
230
+ --color-axis-selected-surface: light-dark(var(--color-neutral-100), var(--color-neutral-900));
231
+ --color-axis-selected-text: light-dark(var(--color-neutral-100), var(--color-neutral-900));
232
+
141
233
  --color-grid-surface: light-dark(var(--color-neutral-50), var(--color-neutral-950));
142
234
  --color-grid-fg: light-dark(var(--color-neutral-950), var(--color-neutral-50));
143
235
  --color-grid-line: light-dark(var(--color-neutral-200), var(--color-neutral-800));
@@ -149,9 +241,4 @@
149
241
  oklch(from var(--color-green-400) l c h / 0.5),
150
242
  oklch(from var(--color-green-600) l c h / 0.5)
151
243
  );
152
-
153
- /* Text */
154
- --color-placeholder: light-dark(var(--color-neutral-400), var(--color-neutral-600));
155
- --color-subdued: light-dark(var(--color-neutral-500), var(--color-neutral-600));
156
- --color-description: light-dark(var(--color-neutral-600), var(--color-neutral-400));
157
244
  }
@@ -31,7 +31,7 @@
31
31
  /** Minimal trim to allow for ring/outline. */
32
32
  --spacing-form-chrome: var(--spacing-trim-xs);
33
33
  --spacing-form-gap: var(--spacing-trim-sm);
34
- --spacing-form-section-gap: var(--spacing-trim-lg);
34
+ --spacing-form-section-gap: var(--spacing-trim-md);
35
35
 
36
36
  /**
37
37
  * Density: md (default).
@@ -84,10 +84,12 @@
84
84
  --dx-lacuna-4: 0.5rem;
85
85
  --dx-lacuna-6: 0.75rem;
86
86
 
87
- --dx-gutter-xs: 0.25rem; /* Input rings */
88
- --dx-gutter-sm: 1rem; /* 16px (sm density card) */
89
- --dx-gutter-md: 2rem; /* 32px (md density card — default) */
90
- --dx-gutter-lg: 2.5rem; /* 40px (dialog, lg density card) */
87
+ /* Form: 8px */
88
+ --dx-gutter-sm: 0.5rem;
89
+ /* Dialog: 16px */
90
+ --dx-gutter-md: 1rem;
91
+ /* Card: 32px */
92
+ --dx-gutter-lg: 2rem;
91
93
  }
92
94
 
93
95
  :root {
package/src/util/index.ts CHANGED
@@ -6,4 +6,6 @@ export * from './elevation';
6
6
  export * from './hash-styles';
7
7
  export * from './mx';
8
8
  export * from './size';
9
+ export * from './tv';
10
+ export * from './tw-merge-config';
9
11
  export * from './valence';
package/src/util/mx.ts CHANGED
@@ -4,45 +4,15 @@
4
4
 
5
5
  import { extendTailwindMerge, validators } from 'tailwind-merge';
6
6
 
7
- type AdditionalClassGroups = 'density' | 'dx-focus-ring';
7
+ import { type AdditionalClassGroups, twMergeConfig } from './tw-merge-config';
8
8
 
9
9
  export const mx = extendTailwindMerge<AdditionalClassGroups>({
10
10
  extend: {
11
+ ...twMergeConfig.extend,
11
12
  classGroups: {
12
- 'font-family': ['font-body', 'font-mono'],
13
- 'font-weight': [
14
- // App weights
15
- 'font-thin',
16
- 'font-extralight',
17
- 'font-light',
18
- 'font-normal',
19
- 'font-medium',
20
- 'font-semibold',
21
- 'font-bold',
22
- 'font-extrabold',
23
- 'font-black',
24
- // Arbitrary numbers
25
- validators.isArbitraryNumber,
26
- ],
27
-
28
- density: ['dx-density-sm', 'dx-density-md', 'dx-density-lg'],
29
-
30
- 'dx-focus-ring': [
31
- 'dx-focus-ring',
32
- 'dx-focus-ring-inset',
33
- 'dx-focus-ring-always',
34
- 'dx-focus-ring-inset-always',
35
- 'dx-focus-ring-group',
36
- 'dx-focus-ring-group-x',
37
- 'dx-focus-ring-group-y',
38
- 'dx-focus-ring-group-always',
39
- 'dx-focus-ring-group-x-always',
40
- 'dx-focus-ring-group-y-always',
41
- 'dx-focus-ring-inset-over-all',
42
- 'dx-focus-ring-inset-over-all-always',
43
- 'dx-focus-ring-main',
44
- 'dx-focus-ring-main-always',
45
- ],
13
+ ...twMergeConfig.extend.classGroups,
14
+ // Arbitrary numeric font-weights require a validator (not expressible as a plain string token).
15
+ 'font-weight': [...twMergeConfig.extend.classGroups['font-weight'], validators.isArbitraryNumber],
46
16
  },
47
17
  },
48
18
  });