@scalepad/ui 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * verify-consumption
4
+ *
5
+ * Smoke test that catches the class of bug a colleague hit when consuming
6
+ * `@scalepad/ui` from a fresh project:
7
+ *
8
+ * - Missing transitive peer dependencies (e.g. @tiptap/core, @tiptap/extensions)
9
+ * - Vite setup gaps (vanilla-extract plugin not wired up, optimizeDeps not configured)
10
+ *
11
+ * What it does:
12
+ * 1. `pnpm pack` the local `@scalepad/ui` and `@scalepad/ui-utils` workspaces.
13
+ * 2. Create a temp Vite + React 19 project in the OS temp dir.
14
+ * 3. Install the local tarballs plus the required peers from the README.
15
+ * 4. Write a minimal `App.tsx` that imports `ThemeProvider`, `Button`,
16
+ * `showToast`, and `SlashRichTextEditor` (so all the optional-peer
17
+ * code paths are exercised).
18
+ * 5. Run `vite build`. If exit code is non-zero, this script fails.
19
+ *
20
+ * Intended use:
21
+ * `pnpm --filter @scalepad/ui run verify:consumption` before publishing
22
+ * a new version. Not wired into CI yet; we'll iterate on speed first.
23
+ */
24
+ import { spawnSync } from 'node:child_process';
25
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, writeFileSync } from 'node:fs';
26
+ import { tmpdir } from 'node:os';
27
+ import { dirname, join, resolve } from 'node:path';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ const __dirname = dirname(fileURLToPath(import.meta.url));
31
+ const pkgRoot = resolve(__dirname, '..');
32
+ const monorepoRoot = resolve(pkgRoot, '..', '..');
33
+ const utilsRoot = resolve(monorepoRoot, 'packages', 'utils');
34
+
35
+ const log = (msg) => console.log(`[verify-consumption] ${msg}`);
36
+ const die = (msg) => {
37
+ console.error(`[verify-consumption] FAIL: ${msg}`);
38
+ process.exit(1);
39
+ };
40
+
41
+ function run(cmd, args, opts = {}) {
42
+ const display = `${cmd} ${args.join(' ')}`;
43
+ log(`$ ${display} (cwd=${opts.cwd ?? process.cwd()})`);
44
+ const result = spawnSync(cmd, args, {
45
+ stdio: 'inherit',
46
+ encoding: 'utf8',
47
+ ...opts,
48
+ });
49
+ if (result.status !== 0) {
50
+ die(`${display} exited with status ${result.status}`);
51
+ }
52
+ return result;
53
+ }
54
+
55
+ function findTarball(dir, prefix) {
56
+ const matches = readdirSync(dir).filter(
57
+ (f) => f.startsWith(prefix) && f.endsWith('.tgz'),
58
+ );
59
+ if (matches.length === 0) {
60
+ die(`no ${prefix}*.tgz produced in ${dir}`);
61
+ }
62
+ if (matches.length > 1) {
63
+ log(`warning: multiple ${prefix}*.tgz found, using newest`);
64
+ }
65
+ // sort by mtime via spawnSync(ls -t) would be heavier; for two same-run packs,
66
+ // alphabetical reverse-sort gets the higher version number.
67
+ matches.sort().reverse();
68
+ return join(dir, matches[0]);
69
+ }
70
+
71
+ // 1. Pack both workspaces.
72
+ log('packing @scalepad/ui and @scalepad/ui-utils...');
73
+ run('pnpm', ['pack', '--pack-destination', pkgRoot], { cwd: pkgRoot });
74
+ run('pnpm', ['pack', '--pack-destination', utilsRoot], { cwd: utilsRoot });
75
+
76
+ const uiTarball = findTarball(pkgRoot, 'scalepad-ui-');
77
+ const utilsTarball = findTarball(utilsRoot, 'scalepad-ui-utils-');
78
+ log(`ui tarball = ${uiTarball}`);
79
+ log(`utils tarball= ${utilsTarball}`);
80
+
81
+ // 2. Make a temp project.
82
+ const projectDir = mkdtempSync(join(tmpdir(), 'scalepad-ui-verify-'));
83
+ log(`temp project = ${projectDir}`);
84
+ mkdirSync(join(projectDir, 'src'), { recursive: true });
85
+
86
+ // 3. Write package.json with the README's required + optional peers.
87
+ const projectPkg = {
88
+ name: 'scalepad-ui-verify',
89
+ private: true,
90
+ version: '0.0.0',
91
+ type: 'module',
92
+ scripts: {
93
+ build: 'vite build',
94
+ },
95
+ // Required peers only. Mantine, recharts, clsx, react-intersection-observer
96
+ // come in as runtime deps of @scalepad/ui — consumers do NOT install them.
97
+ // Tiptap is an optional peer; this smoke test exercises the rich-text editor
98
+ // entry points, so we install the full Tiptap set here.
99
+ dependencies: {
100
+ '@scalepad/ui': `file:${uiTarball}`,
101
+ '@scalepad/ui-utils': `file:${utilsTarball}`,
102
+ '@tanstack/react-query': '^5.0.0',
103
+ '@tanstack/react-table': '^8.21.3',
104
+ '@vanilla-extract/css': '^1.16.2',
105
+ dayjs: '^1.11.19',
106
+ 'lucide-react': '^0.469.0',
107
+ react: '^19.0.0',
108
+ 'react-dom': '^19.0.0',
109
+ '@tiptap/core': '^3.22.3',
110
+ '@tiptap/extensions': '^3.22.3',
111
+ '@tiptap/pm': '^3.22.3',
112
+ '@tiptap/react': '^3.22.3',
113
+ '@tiptap/starter-kit': '^3.22.3',
114
+ '@tiptap/suggestion': '^3.22.3',
115
+ '@tiptap/extension-blockquote': '^3.22.3',
116
+ '@tiptap/extension-bold': '^3.22.3',
117
+ '@tiptap/extension-bubble-menu': '^3.22.3',
118
+ '@tiptap/extension-bullet-list': '^3.22.3',
119
+ '@tiptap/extension-code': '^3.22.3',
120
+ '@tiptap/extension-code-block': '^3.22.3',
121
+ '@tiptap/extension-document': '^3.22.3',
122
+ '@tiptap/extension-dropcursor': '^3.22.3',
123
+ '@tiptap/extension-gapcursor': '^3.22.3',
124
+ '@tiptap/extension-hard-break': '^3.22.3',
125
+ '@tiptap/extension-heading': '^3.22.3',
126
+ '@tiptap/extension-horizontal-rule': '^3.22.3',
127
+ '@tiptap/extension-image': '^3.22.3',
128
+ '@tiptap/extension-italic': '^3.22.3',
129
+ '@tiptap/extension-link': '^3.22.3',
130
+ '@tiptap/extension-list': '^3.22.3',
131
+ '@tiptap/extension-list-item': '^3.22.3',
132
+ '@tiptap/extension-list-keymap': '^3.22.3',
133
+ '@tiptap/extension-ordered-list': '^3.22.3',
134
+ '@tiptap/extension-paragraph': '^3.22.3',
135
+ '@tiptap/extension-placeholder': '^3.22.3',
136
+ '@tiptap/extension-strike': '^3.22.3',
137
+ '@tiptap/extension-text': '^3.22.3',
138
+ '@tiptap/extension-underline': '^3.22.3',
139
+ },
140
+ devDependencies: {
141
+ '@vitejs/plugin-react': '^5.0.4',
142
+ '@types/react': '^19.2.2',
143
+ '@types/react-dom': '^19.2.1',
144
+ typescript: '^6.0.3',
145
+ vite: '^7.1.9',
146
+ },
147
+ };
148
+ writeFileSync(
149
+ join(projectDir, 'package.json'),
150
+ JSON.stringify(projectPkg, null, 2),
151
+ );
152
+
153
+ writeFileSync(
154
+ join(projectDir, 'tsconfig.json'),
155
+ JSON.stringify(
156
+ {
157
+ compilerOptions: {
158
+ target: 'ES2022',
159
+ lib: ['ES2022', 'DOM', 'DOM.Iterable'],
160
+ jsx: 'react-jsx',
161
+ module: 'ESNext',
162
+ moduleResolution: 'bundler',
163
+ strict: true,
164
+ esModuleInterop: true,
165
+ skipLibCheck: true,
166
+ noEmit: true,
167
+ resolveJsonModule: true,
168
+ isolatedModules: true,
169
+ },
170
+ include: ['src'],
171
+ },
172
+ null,
173
+ 2,
174
+ ),
175
+ );
176
+
177
+ writeFileSync(
178
+ join(projectDir, 'vite.config.ts'),
179
+ `import react from '@vitejs/plugin-react';
180
+ import { defineConfig } from 'vite';
181
+ import { scalepadUi } from '@scalepad/ui/vite';
182
+
183
+ export default defineConfig({
184
+ plugins: [react(), scalepadUi()],
185
+ });
186
+ `,
187
+ );
188
+
189
+ writeFileSync(
190
+ join(projectDir, 'index.html'),
191
+ `<!doctype html>
192
+ <html>
193
+ <head><meta charset="utf-8" /><title>verify</title></head>
194
+ <body>
195
+ <div id="root"></div>
196
+ <script type="module" src="/src/main.tsx"></script>
197
+ </body>
198
+ </html>
199
+ `,
200
+ );
201
+
202
+ writeFileSync(
203
+ join(projectDir, 'src', 'main.tsx'),
204
+ `import '@mantine/core/styles.css';
205
+ import { createRoot } from 'react-dom/client';
206
+ import {
207
+ Button,
208
+ Notifications,
209
+ showSuccessToast,
210
+ SlashRichTextEditor,
211
+ ThemeProvider,
212
+ } from '@scalepad/ui';
213
+
214
+ function App() {
215
+ return (
216
+ <ThemeProvider>
217
+ <Notifications />
218
+ <Button onClick={() => showSuccessToast({ title: 'hi', message: 'world' })}>
219
+ Click
220
+ </Button>
221
+ <SlashRichTextEditor commands={[]} placeholder="type" />
222
+ </ThemeProvider>
223
+ );
224
+ }
225
+
226
+ createRoot(document.getElementById('root')!).render(<App />);
227
+ `,
228
+ );
229
+
230
+ // 4. Install. Use npm so we don't depend on the user's pnpm workspace config;
231
+ // file: deps work cleanly under npm and don't leak the monorepo's hoisting.
232
+ log('installing in temp project (this is slow on cold cache)...');
233
+ run('npm', ['install', '--no-audit', '--no-fund', '--legacy-peer-deps=false'], {
234
+ cwd: projectDir,
235
+ });
236
+
237
+ // 5. Build.
238
+ log('building...');
239
+ run('npx', ['vite', 'build'], { cwd: projectDir });
240
+
241
+ log('OK — built successfully.');
242
+ log(`(temp project left at ${projectDir} for inspection; delete when done)`);
@@ -9,9 +9,14 @@ import '@mantine/schedule/styles.css';
9
9
  // oxlint-disable-next-line import/no-unassigned-import
10
10
  import './inter-font';
11
11
 
12
- import type { ReactNode } from 'react';
12
+ import { useMemo, type ReactNode } from 'react';
13
13
 
14
- import { MantineProvider, type MantineThemeOverride } from '@mantine/core';
14
+ import {
15
+ localStorageColorSchemeManager,
16
+ MantineProvider,
17
+ type MantineColorScheme,
18
+ type MantineThemeOverride,
19
+ } from '@mantine/core';
15
20
 
16
21
  import { mantineTheme } from './theme';
17
22
  import { semanticColorsCss } from './tokens/semantic-colors';
@@ -20,15 +25,58 @@ import { semanticTokensCss } from './tokens/semantic-tokens-css';
20
25
  export interface ThemeProviderProps {
21
26
  children: ReactNode;
22
27
  theme?: MantineThemeOverride;
28
+ /**
29
+ * Initial color scheme used when no persisted value is found. Defaults to
30
+ * `'auto'` so a fresh visitor follows their OS appearance preference.
31
+ */
32
+ defaultColorScheme?: MantineColorScheme;
33
+ /**
34
+ * localStorage key used to persist the user's color scheme choice. Apps that
35
+ * embed ThemeProvider can namespace this to keep multiple apps from sharing
36
+ * a key on the same origin.
37
+ */
38
+ colorSchemeStorageKey?: string;
23
39
  }
24
40
 
25
- export function ThemeProvider({ children, theme }: ThemeProviderProps) {
41
+ export function ThemeProvider({
42
+ children,
43
+ theme,
44
+ defaultColorScheme = 'auto',
45
+ colorSchemeStorageKey = 'scalepad-ui.color-scheme',
46
+ }: ThemeProviderProps) {
47
+ const colorSchemeManager = useMemo(
48
+ () => localStorageColorSchemeManager({ key: colorSchemeStorageKey }),
49
+ [colorSchemeStorageKey],
50
+ );
51
+
26
52
  return (
27
- <MantineProvider theme={theme || mantineTheme}>
53
+ <MantineProvider
54
+ theme={theme || mantineTheme}
55
+ defaultColorScheme={defaultColorScheme}
56
+ colorSchemeManager={colorSchemeManager}
57
+ >
28
58
  {/* Inject Figma semantic color tokens as CSS custom properties */}
29
59
  <style dangerouslySetInnerHTML={{ __html: semanticColorsCss }} />
30
60
  {/* Inject semantic radius, spacing, shadows, z-index, font-family */}
31
61
  <style dangerouslySetInnerHTML={{ __html: semanticTokensCss }} />
62
+ {/*
63
+ Global font / glyph rendering. macOS defaults to subpixel
64
+ antialiasing which makes light text on dark surfaces look
65
+ bolded/blurry, and Mantine's reset sometimes overrides body-level
66
+ inline styles. Forcing grayscale antialiasing + optimizeLegibility
67
+ keeps text crisp in both light and dark mode.
68
+ */}
69
+ <style
70
+ dangerouslySetInnerHTML={{
71
+ __html: `
72
+ html, body {
73
+ -webkit-font-smoothing: antialiased;
74
+ -moz-osx-font-smoothing: grayscale;
75
+ text-rendering: optimizeLegibility;
76
+ }
77
+ `,
78
+ }}
79
+ />
32
80
  {/* Override Mantine Table hover to use semantic primary light */}
33
81
  <style
34
82
  dangerouslySetInnerHTML={{
@@ -18,11 +18,58 @@ export interface IconButtonProps extends MantineActionIconProps {
18
18
  variant?: IconButtonVariant;
19
19
  }
20
20
 
21
+ /**
22
+ * Mantine's ActionIcon resolves `--ai-color`, `--ai-bg`, `--ai-hover`, and
23
+ * `--ai-hover-color` to the variant resolver's output and applies them as
24
+ * inline `style="..."` on the rendered button. Inline styles win over our
25
+ * vanilla-extract class CSS, so a class-level `color: tokens.color.text.*`
26
+ * can be silently overridden by Mantine's resolved variant color (especially
27
+ * in dark mode where `subtle + primary` resolves to a pale green that reads
28
+ * as washed out or "black" against the dark surface).
29
+ *
30
+ * Returning a `vars` *function* here pins the CSS variables to our semantic
31
+ * tokens at the same level Mantine writes them — instance vars merge into
32
+ * (and override) the varsResolver output, so `color: var(--ai-color)` in
33
+ * Mantine's class rule picks up our token instead. Mantine 8 invokes the
34
+ * `vars` prop as a function (`vars(theme, props)`), so it must be callable —
35
+ * passing a plain object throws "vars is not a function" at render time.
36
+ */
37
+ type ActionIconVarsFn = MantineActionIconProps['vars'];
38
+
39
+ const ghostVars: ActionIconVarsFn = () => ({
40
+ root: {
41
+ '--ai-color': 'var(--color-text-default)',
42
+ '--ai-bg': 'transparent',
43
+ '--ai-hover': 'var(--color-background-primary-light)',
44
+ '--ai-hover-color': 'var(--color-text-primary-default)',
45
+ },
46
+ });
47
+
48
+ const ghostMutedVars: ActionIconVarsFn = () => ({
49
+ root: {
50
+ '--ai-color': 'var(--color-text-subdued-strong)',
51
+ '--ai-bg': 'transparent',
52
+ '--ai-hover': 'var(--color-background-primary-light)',
53
+ '--ai-hover-color': 'var(--color-text-primary-default)',
54
+ },
55
+ });
56
+
57
+ const outlineVars: ActionIconVarsFn = () => ({
58
+ root: {
59
+ '--ai-color': 'var(--color-text-default)',
60
+ '--ai-bg': 'var(--color-background-default)',
61
+ '--ai-bd': '1px solid var(--color-stroke-default)',
62
+ '--ai-hover': 'var(--color-background-primary-light)',
63
+ '--ai-hover-color': 'var(--color-text-primary-default)',
64
+ },
65
+ });
66
+
21
67
  const IconButtonBase = forwardRef<HTMLButtonElement, IconButtonProps>(
22
68
  ({ variant = 'outline', className, ...props }, ref) => {
23
69
  let mantineVariant: 'outline' | 'subtle' | 'filled' = 'outline';
24
70
  let mantineColor = props.color;
25
71
  let customClassName = className;
72
+ let customVars: ActionIconVarsFn | undefined;
26
73
 
27
74
  if (variant === 'primary') {
28
75
  mantineVariant = 'filled';
@@ -40,6 +87,7 @@ const IconButtonBase = forwardRef<HTMLButtonElement, IconButtonProps>(
40
87
  } else if (variant === 'outline') {
41
88
  mantineVariant = 'outline';
42
89
  customClassName = `${iconButtonStyles.outlineIconButton}${className ? ` ${className}` : ''}`;
90
+ customVars = outlineVars;
43
91
  } else if (variant === 'outline-inverse') {
44
92
  mantineVariant = 'outline';
45
93
  mantineColor = 'white';
@@ -47,9 +95,11 @@ const IconButtonBase = forwardRef<HTMLButtonElement, IconButtonProps>(
47
95
  } else if (variant === 'ghost') {
48
96
  mantineVariant = 'subtle';
49
97
  customClassName = `${iconButtonStyles.ghostIconButton}${className ? ` ${className}` : ''}`;
98
+ customVars = ghostVars;
50
99
  } else if (variant === 'ghost-muted') {
51
100
  mantineVariant = 'subtle';
52
101
  customClassName = `${iconButtonStyles.ghostMutedIconButton}${className ? ` ${className}` : ''}`;
102
+ customVars = ghostMutedVars;
53
103
  }
54
104
 
55
105
  return (
@@ -58,6 +108,7 @@ const IconButtonBase = forwardRef<HTMLButtonElement, IconButtonProps>(
58
108
  variant={mantineVariant}
59
109
  color={mantineColor}
60
110
  className={customClassName}
111
+ vars={customVars}
61
112
  {...props}
62
113
  radius={props.radius ?? 'lg'}
63
114
  />
@@ -1,5 +1,6 @@
1
1
  import { style } from '@vanilla-extract/css';
2
2
 
3
+ import { mantineVars } from '../../theme/mantineVars';
3
4
  import { tokens } from '../../theme/themeContract.css';
4
5
  import { textStyleVariants } from '../../tokens/text-styles';
5
6
 
@@ -59,6 +60,25 @@ export const itemActive = style({
59
60
  backgroundColor: tokens.color.background.primaryLightHover,
60
61
  color: tokens.color.text.primaryDefault,
61
62
  },
63
+ // Dark mode: the global `primaryLight` (greenAlpha 12%) reads as a barely
64
+ // perceivable wash and `primaryDefault` (green[3]) is too pale to register
65
+ // as "selected". The 25-35% tint we initially tried still composited to a
66
+ // dim teal over the dark canvas (≈#243B3B over #161B24). Push the green
67
+ // wash to ~50% / 60% so the active tab reads as a clearly saturated
68
+ // brand surface, and pair with near-white text for a strong "selected"
69
+ // affordance.
70
+ //
71
+ // `mantineVars.darkSelector` already ends in ` &`, so we use it raw
72
+ // rather than appending another ` &` (which would produce an invalid
73
+ // self-descendant selector and silently fail to apply).
74
+ [mantineVars.darkSelector]: {
75
+ backgroundColor: 'rgba(77, 155, 127, 0.5)',
76
+ color: tokens.color.text.title,
77
+ },
78
+ [`${mantineVars.darkSelector}:hover`]: {
79
+ backgroundColor: 'rgba(77, 155, 127, 0.6)',
80
+ color: tokens.color.text.title,
81
+ },
62
82
  },
63
83
  });
64
84
 
package/src/index.ts CHANGED
@@ -475,8 +475,10 @@ export {
475
475
  Textarea,
476
476
  Tooltip,
477
477
  UnstyledButton,
478
+ useMantineColorScheme,
478
479
  } from './mantine';
479
480
  export type {
481
+ MantineColorScheme,
480
482
  PopoverProps,
481
483
  ProgressProps,
482
484
  UnstyledButtonProps,
package/src/mantine.ts CHANGED
@@ -17,8 +17,10 @@ export {
17
17
  Textarea,
18
18
  Tooltip,
19
19
  UnstyledButton,
20
+ useMantineColorScheme,
20
21
  } from '@mantine/core';
21
22
  export type {
23
+ MantineColorScheme,
22
24
  PopoverProps,
23
25
  ProgressProps,
24
26
  UnstyledButtonProps,
@@ -1,7 +1,11 @@
1
1
  /**
2
- * AUTO-GENERATED - DO NOT EDIT
3
- * Generated from: packages/figma-tokens (semantic-colors, border-radius, spacing, shadows)
4
- * Theme contract for vanilla-extract: typed references to CSS variables injected by ThemeProvider.
2
+ * Theme contract for vanilla-extract: typed references to CSS variables
3
+ * injected by ThemeProvider.
4
+ *
5
+ * NOTE: extended by hand to add `background.surface.*`, `background.entity.*`,
6
+ * and the `color.chart.*` group alongside the auto-generated entries from the
7
+ * Figma export. Once the Figma source catches up these can fold back into the
8
+ * generated output.
5
9
  */
6
10
 
7
11
  import { createGlobalThemeContract } from '@vanilla-extract/css';
@@ -63,6 +67,18 @@ export const tokens = createGlobalThemeContract({
63
67
  inverseFilledHover: 'color-background-inverse-filled-hover',
64
68
  input: 'color-background-input',
65
69
  backdrop: 'color-background-backdrop',
70
+ surfaceRaised: 'color-background-surface-raised',
71
+ surfaceOverlay: 'color-background-surface-overlay',
72
+ entityTaskLight: 'color-background-entity-task-light',
73
+ entityTaskIcon: 'color-background-entity-task-icon',
74
+ entityInitiativeLight: 'color-background-entity-initiative-light',
75
+ entityInitiativeIcon: 'color-background-entity-initiative-icon',
76
+ entityGoalLight: 'color-background-entity-goal-light',
77
+ entityGoalIcon: 'color-background-entity-goal-icon',
78
+ entityMeetingLight: 'color-background-entity-meeting-light',
79
+ entityMeetingIcon: 'color-background-entity-meeting-icon',
80
+ entityDeliverableLight: 'color-background-entity-deliverable-light',
81
+ entityDeliverableIcon: 'color-background-entity-deliverable-icon',
66
82
  },
67
83
  stroke: {
68
84
  default: 'color-stroke-default',
@@ -97,6 +113,14 @@ export const tokens = createGlobalThemeContract({
97
113
  primaryStrong: 'color-icon-primary-strong',
98
114
  medium: 'color-icon-medium',
99
115
  },
116
+ chart: {
117
+ series1: 'color-chart-series-1',
118
+ series2: 'color-chart-series-2',
119
+ series3: 'color-chart-series-3',
120
+ series4: 'color-chart-series-4',
121
+ series5: 'color-chart-series-5',
122
+ series6: 'color-chart-series-6',
123
+ },
100
124
  },
101
125
  radius: {
102
126
  xs: 'radius-xs',
@@ -1,9 +1,10 @@
1
1
  /**
2
- * AUTO-GENERATED - DO NOT EDIT
3
- * Generated from: packages/figma-tokens/source/semantic-colors.json
4
- *
5
2
  * Typed color token unions grouped by their intended usage.
6
3
  * Use these types to restrict color props to only valid tokens.
4
+ *
5
+ * NOTE: extended by hand to add `background.surface.*`, `background.entity.*`,
6
+ * and the new `ChartColor` type alongside the auto-generated entries from the
7
+ * Figma export of `packages/figma-tokens/source/semantic-colors.json`.
7
8
  */
8
9
 
9
10
  /** Text/foreground color tokens (for the `c` prop on text elements) */
@@ -62,7 +63,19 @@ export type BackgroundColor =
62
63
  | 'background.inverse.filled'
63
64
  | 'background.inverse.filled-hover'
64
65
  | 'background.input'
65
- | 'background.backdrop';
66
+ | 'background.backdrop'
67
+ | 'background.surface.raised'
68
+ | 'background.surface.overlay'
69
+ | 'background.entity.task.light'
70
+ | 'background.entity.task.icon'
71
+ | 'background.entity.initiative.light'
72
+ | 'background.entity.initiative.icon'
73
+ | 'background.entity.goal.light'
74
+ | 'background.entity.goal.icon'
75
+ | 'background.entity.meeting.light'
76
+ | 'background.entity.meeting.icon'
77
+ | 'background.entity.deliverable.light'
78
+ | 'background.entity.deliverable.icon';
66
79
 
67
80
  /** Stroke/border color tokens (for borders and dividers) */
68
81
  export type StrokeColor =
@@ -99,9 +112,19 @@ export type IconColor =
99
112
  | 'icon.primary.strong'
100
113
  | 'icon.medium';
101
114
 
115
+ /** Chart series color tokens (for Recharts series colors, legend swatches, etc.) */
116
+ export type ChartColor =
117
+ | 'chart.series.1'
118
+ | 'chart.series.2'
119
+ | 'chart.series.3'
120
+ | 'chart.series.4'
121
+ | 'chart.series.5'
122
+ | 'chart.series.6';
123
+
102
124
  /** Any semantic color token */
103
125
  export type SemanticColor =
104
126
  | TextColor
105
127
  | BackgroundColor
106
128
  | StrokeColor
107
- | IconColor;
129
+ | IconColor
130
+ | ChartColor;
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Color primitives synced from Figma Design System (VCLfybgU3OaUUPrQdBaVmP).
3
3
  * Figma source of truth: Primitives collection.
4
+ *
5
+ * NOTE: `dark[]`, `greenAlpha`, and `grapeAlpha` have been hand-tuned in code
6
+ * ahead of the Figma back-sync to give dark mode real surface depth and proper
7
+ * brand-tinted alphas. Once those values land in Figma the file can revert to
8
+ * fully auto-generated.
4
9
  */
5
10
 
6
11
  import type { MantineColorsTuple } from '@mantine/core';
@@ -21,6 +26,14 @@ export const green: MantineColorsTuple = [
21
26
  '#005a3e',
22
27
  ];
23
28
 
29
+ export const greenAlpha = {
30
+ '08': 'rgba(77, 155, 127, 0.08)',
31
+ '10': 'rgba(77, 155, 127, 0.10)',
32
+ '12': 'rgba(77, 155, 127, 0.12)',
33
+ '15': 'rgba(77, 155, 127, 0.15)',
34
+ '20': 'rgba(77, 155, 127, 0.20)',
35
+ } as const;
36
+
24
37
  export const blue: MantineColorsTuple = [
25
38
  '#e7f5ff',
26
39
  '#d0ebff',
@@ -72,19 +85,41 @@ export const grayAlpha = {
72
85
  '9-60': 'rgba(17, 24, 39, 0.60)',
73
86
  } as const;
74
87
 
88
+ /**
89
+ * Dark neutral scale tuned for sophisticated dark-mode surface depth (cool
90
+ * near-black with a subtle blue cast). 0 → lightest, 9 → darkest, matching
91
+ * the Mantine palette convention so dark-mode semantic mappings flip naturally
92
+ * against the light gray scale.
93
+ *
94
+ * Surface roles per index:
95
+ * 0 (#e7eaf2) headings, inverse content
96
+ * 1 (#c8cdd9) body text
97
+ * 2 (#8a92a6) subdued text
98
+ * 3 (#4a5163) disabled text, subdued borders
99
+ * 4 (#323847) default borders
100
+ * 5 (#252b38) hover / pressed surface
101
+ * 6 (#1c222d) elevated surface (popovers, dropdowns)
102
+ * 7 (#161b24) default surface (cards, panels)
103
+ * 8 (#11151c) body background
104
+ * 9 (#0b0e14) outer canvas / chrome
105
+ */
75
106
  export const dark: MantineColorsTuple = [
76
- '#c9c9c9',
77
- '#b8b8b8',
78
- '#828282',
79
- '#696969',
80
- '#424242',
81
- '#3b3b3b',
82
- '#2e2e2e',
83
- '#242424',
84
- '#1f1f1f',
85
- '#141414',
107
+ '#e7eaf2',
108
+ '#c8cdd9',
109
+ '#8a92a6',
110
+ '#4a5163',
111
+ '#323847',
112
+ '#252b38',
113
+ '#1c222d',
114
+ '#161b24',
115
+ '#11151c',
116
+ '#0b0e14',
86
117
  ];
87
118
 
119
+ export const darkAlpha = {
120
+ '70': 'rgba(11, 14, 20, 0.70)',
121
+ } as const;
122
+
88
123
  export const primary: MantineColorsTuple = [
89
124
  '#edf7f3',
90
125
  '#d5f0e5',
@@ -164,6 +199,13 @@ export const grape: MantineColorsTuple = [
164
199
  '#862e9c',
165
200
  ];
166
201
 
202
+ export const grapeAlpha = {
203
+ '10': 'rgba(190, 75, 219, 0.10)',
204
+ '12': 'rgba(190, 75, 219, 0.12)',
205
+ '15': 'rgba(190, 75, 219, 0.15)',
206
+ '20': 'rgba(190, 75, 219, 0.20)',
207
+ } as const;
208
+
167
209
  export const cyan: MantineColorsTuple = [
168
210
  '#e3fafc',
169
211
  '#c5f6fa',