@dxos/ui-theme 0.9.1-main.c7dcc2e112 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/ui-theme",
3
- "version": "0.9.1-main.c7dcc2e112",
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.1-main.c7dcc2e112",
54
- "@dxos/node-std": "0.9.1-main.c7dcc2e112",
55
- "@dxos/util": "0.9.1-main.c7dcc2e112"
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
61
  "esbuild": "0.28.1",
61
62
  "vite": "^8.0.16",
62
- "@dxos/ui-types": "0.9.1-main.c7dcc2e112"
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 */
@@ -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
  }
@@ -26,13 +26,14 @@
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));
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));
36
37
 
37
38
  /**
38
39
  * Elevation 0
@@ -50,71 +51,75 @@
50
51
  * Elevation 1
51
52
  */
52
53
 
53
- --color-base-surface: var(--dx-elevation-1);
54
+ --color-card-surface: var(--dx-elevation-1);
55
+
56
+ /**
57
+ * Elevation 2
58
+ */
59
+
60
+ --color-base-surface: var(--dx-elevation-2);
54
61
  --color-base-fg: light-dark(var(--color-neutral-950), var(--color-neutral-150));
55
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
+
56
67
  /**
57
- * Elevation 2
68
+ * Elevation 3
58
69
  */
59
70
 
60
- --color-header-surface: var(--dx-elevation-2);
71
+ --color-header-surface: var(--dx-elevation-3);
61
72
 
62
- --color-sidebar-surface: var(--dx-elevation-2);
73
+ --color-sidebar-surface: var(--dx-elevation-3);
63
74
  /* Sidebar body text. Default sits one ramp stop below the current/selected emphasis. */
64
75
  --color-sidebar-fg: light-dark(var(--color-neutral-700), var(--color-neutral-250));
65
76
  --color-sidebar-current-fg: light-dark(var(--color-neutral-950), var(--color-neutral-75));
66
77
 
67
78
  /* Sidebar / panel layout levels */
68
- --color-l0-surface: var(--dx-elevation-1);
69
- --color-l1-surface: var(--dx-elevation-2);
70
- --color-r0-surface: var(--dx-elevation-2);
71
- --color-r1-surface: var(--dx-elevation-2);
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);
72
83
 
73
84
  /**
74
- * Elevation 3
85
+ * Elevation 4
75
86
  */
76
87
 
77
- --color-card-surface: var(--dx-elevation-3);
78
-
79
- /* Map attention to focus-surface logic so that elements acting as active attention zones highlight correctly on focus. */
80
- --color-attention-surface: var(--dx-elevation-3);
81
- --color-attention-contains: oklch(from var(--color-accent-bg) l c h / 0.3);
88
+ --color-toolbar-surface: var(--dx-elevation-4);
82
89
 
83
90
  /**
84
- * Elevation 4
91
+ * Elevation 5
85
92
  */
86
93
 
87
94
  /* Focus — a raised content surface (one level above card) so an attended pane reads as lifted. */
88
- --color-focus-surface: var(--dx-elevation-4);
95
+ --color-focus-surface: var(--dx-elevation-5);
89
96
  --color-focus-ring: light-dark(var(--color-blue-400), var(--color-blue-600));
90
97
  --color-focus-ring-subtle: light-dark(var(--color-neutral-300), var(--color-neutral-600));
91
98
 
92
- --color-group-surface: var(--dx-elevation-4);
99
+ --color-group-surface: var(--dx-elevation-5);
93
100
  /* Subtle alternate of group-surface for zebra striping (e.g. calendar months) — offset off
94
101
  * elevation-4 via relative oklch so it tracks the ramp: darker in light mode, lighter in dark. */
95
102
  --color-group-alt-surface: light-dark(
96
- oklch(from var(--dx-elevation-4) calc(l - 0.03) c h),
97
- oklch(from var(--dx-elevation-4) calc(l + 0.03) c h)
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)
98
105
  );
99
106
 
100
- --color-input-surface: var(--dx-elevation-4);
107
+ --color-input-surface: var(--dx-elevation-5);
101
108
 
102
109
  /**
103
- * Elevation 5
110
+ * Elevation 6
104
111
  */
105
112
 
106
- --color-toolbar-surface: var(--dx-elevation-5);
107
-
108
- --color-modal-surface: var(--dx-elevation-5);
113
+ --color-modal-surface: var(--dx-elevation-6);
109
114
 
110
115
  /**
111
- * Elevation 6
116
+ * Elevation 7
112
117
  */
113
118
 
114
- --color-popover-surface: var(--dx-elevation-6);
119
+ --color-popover-surface: var(--dx-elevation-7);
115
120
 
116
121
  /**
117
- * Elevation 7
122
+ * Elevation 8
118
123
  */
119
124
 
120
125
  /**
@@ -198,9 +203,10 @@
198
203
  --color-un-accent-hover: var(--color-neutral-500);
199
204
 
200
205
  /* Separator */
201
- --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));
202
207
  --color-separator: light-dark(var(--color-neutral-200), var(--color-neutral-700));
203
- --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));
204
210
  --color-active-separator: light-dark(var(--color-blue-500), var(--color-blue-500));
205
211
 
206
212
  /* Scrollbar */
@@ -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
  });
@@ -0,0 +1,38 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { mx } from './mx';
8
+ import { bridgeTv, tv } from './tv';
9
+
10
+ describe('tv', () => {
11
+ test('resolves standard tailwind conflicts like mx', ({ expect }) => {
12
+ const recipe = tv({ base: 'text-sm', variants: { big: { true: 'text-lg' } } });
13
+ expect(recipe({ big: true })).toBe(mx('text-sm', 'text-lg'));
14
+ expect(recipe({ big: true })).toContain('text-lg');
15
+ expect(recipe({ big: true })).not.toContain('text-sm');
16
+ });
17
+
18
+ test('keeps dxos custom color tokens (text-base-fg over text-description)', ({ expect }) => {
19
+ const recipe = tv({ base: 'text-description', variants: { strong: { true: 'text-base-fg' } } });
20
+ expect(recipe({ strong: true })).toBe(mx('text-description', 'text-base-fg'));
21
+ expect(recipe({ strong: true })).toContain('text-base-fg');
22
+ });
23
+ });
24
+
25
+ describe('bridgeTv', () => {
26
+ test('adapts a slots recipe into Theme<P> functions with overrides', ({ expect }) => {
27
+ const recipe = tv({
28
+ slots: { root: 'p-1', label: 'text-sm' },
29
+ variants: { variant: { settings: { root: 'p-4', label: 'text-lg' } } },
30
+ });
31
+ const theme = bridgeTv(recipe, ['root', 'label']);
32
+ expect(typeof theme.root).toBe('function');
33
+ expect(theme.root({ variant: 'settings' })).toContain('p-4');
34
+ // consumer override wins via per-slot merge.
35
+ expect(theme.label({ variant: 'settings' }, 'text-xl')).toContain('text-xl');
36
+ expect(theme.label({ variant: 'settings' }, 'text-xl')).not.toContain('text-lg');
37
+ });
38
+ });
package/src/util/tv.ts ADDED
@@ -0,0 +1,45 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { createTV } from 'tailwind-variants';
6
+
7
+ import { type ClassNameArray, type ClassNameValue, type Theme } from '@dxos/ui-types';
8
+
9
+ import { twMergeConfig } from './tw-merge-config';
10
+
11
+ export type { VariantProps } from 'tailwind-variants';
12
+
13
+ /**
14
+ * Shared tailwind-variants instance bound to the dxos tailwind-merge config (see {@link twMergeConfig}),
15
+ * so recipes resolve class conflicts identically to {@link mx}. All component theme recipes import this,
16
+ * never the bare `tailwind-variants` package.
17
+ */
18
+ export const tv = createTV({ twMerge: true, twMergeConfig });
19
+
20
+ type SlotsRecipe<P extends Record<string, any>, S extends string> = (
21
+ props?: P,
22
+ ) => Record<S, (opts?: { class?: ClassNameValue }) => string>;
23
+
24
+ /**
25
+ * The {@link Theme} sub-tree produced by {@link bridgeTv}, with each slot's resolver explicitly typed
26
+ * so call sites (and tests) stay fully typed instead of falling back to `any`.
27
+ */
28
+ export type BridgedTheme<P extends Record<string, any>, S extends string> = Theme<P> & {
29
+ [K in S]: (styleProps: P, ...etc: ClassNameArray) => string;
30
+ };
31
+
32
+ /**
33
+ * Adapt a tailwind-variants slots recipe into the existing {@link Theme} shape (a map of
34
+ * {@link import('@dxos/ui-types').ComponentFunction}) so it can register in the `tx` theme tree and be
35
+ * consumed via `tx('component.slot', styleProps, ...classNames)`. Slots are enumerated explicitly (not
36
+ * via Proxy) so unknown paths resolve to `undefined` exactly as `getDeep` does today, and the result is
37
+ * a plain, inspectable object. Derive `slots` from `Object.keys(recipe())` at the call site.
38
+ */
39
+ export const bridgeTv = <P extends Record<string, any>, S extends string>(
40
+ recipe: SlotsRecipe<P, S>,
41
+ slots: readonly S[],
42
+ ): BridgedTheme<P, S> =>
43
+ Object.fromEntries(
44
+ slots.map((slot) => [slot, (styleProps: P, ...etc: ClassNameArray) => recipe(styleProps)[slot]({ class: etc })]),
45
+ ) as BridgedTheme<P, S>;
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ /**
6
+ * Shared tailwind-merge configuration. The single source for both {@link mx} (extendTailwindMerge)
7
+ * and the {@link tv} instance (createTV `twMergeConfig`) so conflict resolution can't drift —
8
+ * notably for dxos custom tokens (`text-base-fg`, density, focus-ring).
9
+ */
10
+ export const twMergeConfig = {
11
+ extend: {
12
+ classGroups: {
13
+ 'font-family': ['font-body', 'font-mono'],
14
+ 'font-weight': [
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
+ ],
25
+ 'density': ['dx-density-sm', 'dx-density-md', 'dx-density-lg'],
26
+ 'dx-focus-ring': [
27
+ 'dx-focus-ring',
28
+ 'dx-focus-ring-inset',
29
+ 'dx-focus-ring-always',
30
+ 'dx-focus-ring-inset-always',
31
+ 'dx-focus-ring-group',
32
+ 'dx-focus-ring-group-x',
33
+ 'dx-focus-ring-group-y',
34
+ 'dx-focus-ring-group-always',
35
+ 'dx-focus-ring-group-x-always',
36
+ 'dx-focus-ring-group-y-always',
37
+ 'dx-focus-ring-inset-over-all',
38
+ 'dx-focus-ring-inset-over-all-always',
39
+ 'dx-focus-ring-main',
40
+ 'dx-focus-ring-main-always',
41
+ ],
42
+ },
43
+ },
44
+ } as const;
45
+
46
+ export type AdditionalClassGroups = 'density' | 'dx-focus-ring';