@scalepad/ui 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +264 -28
- package/package.json +150 -36
- package/scripts/verify-consumption.mjs +275 -0
- package/src/ThemeProvider.tsx +52 -4
- package/src/components/Anchor/Anchor.css.ts +1 -1
- package/src/components/ColorInput/ColorInput.figma.tsx +32 -0
- package/src/components/ColorInput/ColorInput.tsx +94 -0
- package/src/components/ColorInput/index.ts +6 -0
- package/src/components/IconButton/IconButton.tsx +51 -0
- package/src/components/Select/Select.css.ts +8 -3
- package/src/components/Select/Select.tsx +11 -1
- package/src/components/SubNavigation/SubNavigation.css.ts +20 -0
- package/src/index.ts +8 -0
- package/src/mantine.ts +2 -0
- package/src/theme/themeContract.css.ts +27 -3
- package/src/tokens/color-types.ts +28 -5
- package/src/tokens/colors.ts +52 -10
- package/src/tokens/index.ts +6 -2
- package/src/tokens/semantic-colors.ts +177 -73
- package/src/tokens/semantic-tokens-css.ts +34 -12
- package/src/tokens/shadows.ts +30 -6
- package/src/tokens/text-styles.ts +31 -2
- package/src/vite.d.ts +37 -0
- package/src/vite.js +120 -0
|
@@ -0,0 +1,275 @@
|
|
|
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 runCapture(cmd, args, opts = {}) {
|
|
56
|
+
const display = `${cmd} ${args.join(' ')}`;
|
|
57
|
+
log(`$ ${display} (cwd=${opts.cwd ?? process.cwd()})`);
|
|
58
|
+
const result = spawnSync(cmd, args, {
|
|
59
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
60
|
+
encoding: 'utf8',
|
|
61
|
+
...opts,
|
|
62
|
+
});
|
|
63
|
+
process.stdout.write(result.stdout ?? '');
|
|
64
|
+
process.stderr.write(result.stderr ?? '');
|
|
65
|
+
if (result.status !== 0) {
|
|
66
|
+
die(`${display} exited with status ${result.status}`);
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function findTarball(dir, prefix) {
|
|
72
|
+
const matches = readdirSync(dir).filter(
|
|
73
|
+
(f) => f.startsWith(prefix) && f.endsWith('.tgz'),
|
|
74
|
+
);
|
|
75
|
+
if (matches.length === 0) {
|
|
76
|
+
die(`no ${prefix}*.tgz produced in ${dir}`);
|
|
77
|
+
}
|
|
78
|
+
if (matches.length > 1) {
|
|
79
|
+
log(`warning: multiple ${prefix}*.tgz found, using newest`);
|
|
80
|
+
}
|
|
81
|
+
// sort by mtime via spawnSync(ls -t) would be heavier; for two same-run packs,
|
|
82
|
+
// alphabetical reverse-sort gets the higher version number.
|
|
83
|
+
matches.sort().reverse();
|
|
84
|
+
return join(dir, matches[0]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 1. Pack both workspaces.
|
|
88
|
+
log('packing @scalepad/ui and @scalepad/ui-utils...');
|
|
89
|
+
run('pnpm', ['pack', '--pack-destination', pkgRoot], { cwd: pkgRoot });
|
|
90
|
+
run('pnpm', ['pack', '--pack-destination', utilsRoot], { cwd: utilsRoot });
|
|
91
|
+
|
|
92
|
+
const uiTarball = findTarball(pkgRoot, 'scalepad-ui-');
|
|
93
|
+
const utilsTarball = findTarball(utilsRoot, 'scalepad-ui-utils-');
|
|
94
|
+
log(`ui tarball = ${uiTarball}`);
|
|
95
|
+
log(`utils tarball= ${utilsTarball}`);
|
|
96
|
+
|
|
97
|
+
// 2. Make a temp project.
|
|
98
|
+
const projectDir = mkdtempSync(join(tmpdir(), 'scalepad-ui-verify-'));
|
|
99
|
+
log(`temp project = ${projectDir}`);
|
|
100
|
+
mkdirSync(join(projectDir, 'src'), { recursive: true });
|
|
101
|
+
|
|
102
|
+
// 3. Write package.json with the README's required + optional peers.
|
|
103
|
+
const projectPkg = {
|
|
104
|
+
name: 'scalepad-ui-verify',
|
|
105
|
+
private: true,
|
|
106
|
+
version: '0.0.0',
|
|
107
|
+
type: 'module',
|
|
108
|
+
scripts: {
|
|
109
|
+
build: 'vite build',
|
|
110
|
+
},
|
|
111
|
+
// Required peers only. Mantine, recharts, clsx, react-intersection-observer
|
|
112
|
+
// come in as runtime deps of @scalepad/ui — consumers do NOT install them.
|
|
113
|
+
// Tiptap is an optional peer; this smoke test exercises the rich-text editor
|
|
114
|
+
// entry points, so we install the full Tiptap set here.
|
|
115
|
+
dependencies: {
|
|
116
|
+
'@scalepad/ui': `file:${uiTarball}`,
|
|
117
|
+
'@scalepad/ui-utils': `file:${utilsTarball}`,
|
|
118
|
+
'@tanstack/react-query': '^5.0.0',
|
|
119
|
+
'@tanstack/react-table': '^8.21.3',
|
|
120
|
+
'@vanilla-extract/css': '^1.16.2',
|
|
121
|
+
dayjs: '^1.11.19',
|
|
122
|
+
'lucide-react': '^0.469.0',
|
|
123
|
+
react: '^19.0.0',
|
|
124
|
+
'react-dom': '^19.0.0',
|
|
125
|
+
'@tiptap/core': '^3.22.3',
|
|
126
|
+
'@tiptap/extensions': '^3.22.3',
|
|
127
|
+
'@tiptap/pm': '^3.22.3',
|
|
128
|
+
'@tiptap/react': '^3.22.3',
|
|
129
|
+
'@tiptap/starter-kit': '^3.22.3',
|
|
130
|
+
'@tiptap/suggestion': '^3.22.3',
|
|
131
|
+
'@tiptap/extension-blockquote': '^3.22.3',
|
|
132
|
+
'@tiptap/extension-bold': '^3.22.3',
|
|
133
|
+
'@tiptap/extension-bubble-menu': '^3.22.3',
|
|
134
|
+
'@tiptap/extension-bullet-list': '^3.22.3',
|
|
135
|
+
'@tiptap/extension-code': '^3.22.3',
|
|
136
|
+
'@tiptap/extension-code-block': '^3.22.3',
|
|
137
|
+
'@tiptap/extension-document': '^3.22.3',
|
|
138
|
+
'@tiptap/extension-dropcursor': '^3.22.3',
|
|
139
|
+
'@tiptap/extension-gapcursor': '^3.22.3',
|
|
140
|
+
'@tiptap/extension-hard-break': '^3.22.3',
|
|
141
|
+
'@tiptap/extension-heading': '^3.22.3',
|
|
142
|
+
'@tiptap/extension-horizontal-rule': '^3.22.3',
|
|
143
|
+
'@tiptap/extension-image': '^3.22.3',
|
|
144
|
+
'@tiptap/extension-italic': '^3.22.3',
|
|
145
|
+
'@tiptap/extension-link': '^3.22.3',
|
|
146
|
+
'@tiptap/extension-list': '^3.22.3',
|
|
147
|
+
'@tiptap/extension-list-item': '^3.22.3',
|
|
148
|
+
'@tiptap/extension-list-keymap': '^3.22.3',
|
|
149
|
+
'@tiptap/extension-ordered-list': '^3.22.3',
|
|
150
|
+
'@tiptap/extension-paragraph': '^3.22.3',
|
|
151
|
+
'@tiptap/extension-placeholder': '^3.22.3',
|
|
152
|
+
'@tiptap/extension-strike': '^3.22.3',
|
|
153
|
+
'@tiptap/extension-text': '^3.22.3',
|
|
154
|
+
'@tiptap/extension-underline': '^3.22.3',
|
|
155
|
+
},
|
|
156
|
+
devDependencies: {
|
|
157
|
+
'@vitejs/plugin-react': '^5.0.4',
|
|
158
|
+
'@types/react': '^19.2.2',
|
|
159
|
+
'@types/react-dom': '^19.2.1',
|
|
160
|
+
typescript: '^6.0.3',
|
|
161
|
+
vite: '^7.1.9',
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
writeFileSync(
|
|
165
|
+
join(projectDir, 'package.json'),
|
|
166
|
+
JSON.stringify(projectPkg, null, 2),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
writeFileSync(
|
|
170
|
+
join(projectDir, 'tsconfig.json'),
|
|
171
|
+
JSON.stringify(
|
|
172
|
+
{
|
|
173
|
+
compilerOptions: {
|
|
174
|
+
target: 'ES2022',
|
|
175
|
+
lib: ['ES2022', 'DOM', 'DOM.Iterable'],
|
|
176
|
+
jsx: 'react-jsx',
|
|
177
|
+
module: 'ESNext',
|
|
178
|
+
moduleResolution: 'bundler',
|
|
179
|
+
strict: true,
|
|
180
|
+
esModuleInterop: true,
|
|
181
|
+
skipLibCheck: true,
|
|
182
|
+
noEmit: true,
|
|
183
|
+
resolveJsonModule: true,
|
|
184
|
+
isolatedModules: true,
|
|
185
|
+
},
|
|
186
|
+
include: ['src'],
|
|
187
|
+
},
|
|
188
|
+
null,
|
|
189
|
+
2,
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
writeFileSync(
|
|
194
|
+
join(projectDir, 'vite.config.ts'),
|
|
195
|
+
`import react from '@vitejs/plugin-react';
|
|
196
|
+
import { defineConfig } from 'vite';
|
|
197
|
+
import { scalepadUi } from '@scalepad/ui/vite';
|
|
198
|
+
|
|
199
|
+
export default defineConfig({
|
|
200
|
+
plugins: [react(), scalepadUi()],
|
|
201
|
+
});
|
|
202
|
+
`,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
writeFileSync(
|
|
206
|
+
join(projectDir, 'index.html'),
|
|
207
|
+
`<!doctype html>
|
|
208
|
+
<html>
|
|
209
|
+
<head><meta charset="utf-8" /><title>verify</title></head>
|
|
210
|
+
<body>
|
|
211
|
+
<div id="root"></div>
|
|
212
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
213
|
+
</body>
|
|
214
|
+
</html>
|
|
215
|
+
`,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
writeFileSync(
|
|
219
|
+
join(projectDir, 'src', 'main.tsx'),
|
|
220
|
+
`import '@mantine/core/styles.css';
|
|
221
|
+
import { createRoot } from 'react-dom/client';
|
|
222
|
+
import {
|
|
223
|
+
Button,
|
|
224
|
+
Notifications,
|
|
225
|
+
showSuccessToast,
|
|
226
|
+
SlashRichTextEditor,
|
|
227
|
+
ThemeProvider,
|
|
228
|
+
} from '@scalepad/ui';
|
|
229
|
+
|
|
230
|
+
function App() {
|
|
231
|
+
return (
|
|
232
|
+
<ThemeProvider>
|
|
233
|
+
<Notifications />
|
|
234
|
+
<Button onClick={() => showSuccessToast({ title: 'hi', message: 'world' })}>
|
|
235
|
+
Click
|
|
236
|
+
</Button>
|
|
237
|
+
<SlashRichTextEditor commands={[]} placeholder="type" />
|
|
238
|
+
</ThemeProvider>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
createRoot(document.getElementById('root')!).render(<App />);
|
|
243
|
+
`,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// 4. Install. Use npm so we don't depend on the user's pnpm workspace config;
|
|
247
|
+
// file: deps work cleanly under npm and don't leak the monorepo's hoisting.
|
|
248
|
+
log('installing in temp project (this is slow on cold cache)...');
|
|
249
|
+
run('npm', ['install', '--no-audit', '--no-fund', '--legacy-peer-deps=false'], {
|
|
250
|
+
cwd: projectDir,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// 5. Build. Capture output so we can assert no known-problematic Vite warnings
|
|
254
|
+
// snuck back in (they don't fail the build by default, so a plain run won't
|
|
255
|
+
// catch a regression).
|
|
256
|
+
log('building...');
|
|
257
|
+
const buildResult = runCapture('npx', ['vite', 'build'], { cwd: projectDir });
|
|
258
|
+
const buildOutput = `${buildResult.stdout ?? ''}\n${buildResult.stderr ?? ''}`;
|
|
259
|
+
|
|
260
|
+
// Regression guard for the rrule / @mantine/schedule warning. The
|
|
261
|
+
// `scalepadUi()` preset aliases `rrule` to its CJS UMD entry so this warning
|
|
262
|
+
// shouldn't appear. See README → Troubleshooting → IMPORT_IS_UNDEFINED.
|
|
263
|
+
if (
|
|
264
|
+
buildOutput.includes('IMPORT_IS_UNDEFINED') ||
|
|
265
|
+
/node_modules\/rrule\/dist\/esm\/index\.js/.test(buildOutput)
|
|
266
|
+
) {
|
|
267
|
+
die(
|
|
268
|
+
'detected rrule IMPORT_IS_UNDEFINED warning in build output — the ' +
|
|
269
|
+
'scalepadUi() preset should be aliasing rrule to rrule/dist/es5/rrule.js. ' +
|
|
270
|
+
'See packages/ui/src/vite.js.',
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
log('OK — built successfully (no known-problematic warnings).');
|
|
275
|
+
log(`(temp project left at ${projectDir} for inspection; delete when done)`);
|
package/src/ThemeProvider.tsx
CHANGED
|
@@ -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
|
|
12
|
+
import { useMemo, type ReactNode } from 'react';
|
|
13
13
|
|
|
14
|
-
import {
|
|
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({
|
|
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
|
|
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={{
|
|
@@ -53,7 +53,7 @@ export const root = style({
|
|
|
53
53
|
* drift.
|
|
54
54
|
*/
|
|
55
55
|
export const variant = styleVariants(textStyleVariants, styles => ({
|
|
56
|
-
fontFamily: styles.fontFamily,
|
|
56
|
+
...(styles.fontFamily != null && { fontFamily: styles.fontFamily }),
|
|
57
57
|
fontWeight: styles.fontWeight,
|
|
58
58
|
fontSize: styles.fontSize,
|
|
59
59
|
lineHeight: styles.lineHeight,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import figma from '@figma/code-connect';
|
|
2
|
+
|
|
3
|
+
import { ColorInput } from './ColorInput';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Code Connect mapping for the LM Design System `ColorInput` (node 4265:237).
|
|
7
|
+
*
|
|
8
|
+
* The Figma component_set has `Size` (Small / Regular / Large) and `State`
|
|
9
|
+
* (Empty / Value / Focus / Error / Error Focus / Disabled) axes. We
|
|
10
|
+
* intentionally don't map the size axis: in code the component follows the
|
|
11
|
+
* `TextInput` chrome (single default size, overridable via `size`), and
|
|
12
|
+
* Figma will be re-aligned to match later. Only the boolean-ish state axes
|
|
13
|
+
* map cleanly today (`disabled`, `error`).
|
|
14
|
+
*/
|
|
15
|
+
figma.connect(
|
|
16
|
+
ColorInput,
|
|
17
|
+
'https://www.figma.com/design/VCLfybgU3OaUUPrQdBaVmP/LM-Design-System?node-id=4265%3A237',
|
|
18
|
+
{
|
|
19
|
+
props: {
|
|
20
|
+
disabled: figma.enum('State', {
|
|
21
|
+
Disabled: true,
|
|
22
|
+
}),
|
|
23
|
+
error: figma.enum('State', {
|
|
24
|
+
Error: 'Invalid color',
|
|
25
|
+
'Error Focus': 'Invalid color',
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
example: props => (
|
|
29
|
+
<ColorInput disabled={props.disabled} error={props.error} />
|
|
30
|
+
),
|
|
31
|
+
},
|
|
32
|
+
);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ColorInput component
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper around Mantine's `ColorInput` that mirrors the conventions of
|
|
5
|
+
* our `TextInput` wrapper (`size='sm'`, `radius='lg'`, `format='hex'`) and
|
|
6
|
+
* pre-loads the swatch grid with `AVATAR_PALETTE` — the 50-color list
|
|
7
|
+
* `@scalepad/ui` already uses for identity chips. Reusing the same palette
|
|
8
|
+
* here keeps every "pick a color" surface in the app consistent without
|
|
9
|
+
* making consumers wire `swatches` themselves.
|
|
10
|
+
*
|
|
11
|
+
* Visual chrome (height, padding, radius, focus ring, error state, disabled
|
|
12
|
+
* state) intentionally follows `TextInput` rather than the standalone Figma
|
|
13
|
+
* ColorInput frame. Figma will be aligned later.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <ColorInput
|
|
18
|
+
* label="Brand color"
|
|
19
|
+
* value={color}
|
|
20
|
+
* onChange={setColor}
|
|
21
|
+
* />
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example Custom swatches
|
|
25
|
+
* ```tsx
|
|
26
|
+
* <ColorInput
|
|
27
|
+
* swatches={['#ff0000', '#00ff00', '#0000ff']}
|
|
28
|
+
* swatchesPerRow={3}
|
|
29
|
+
* />
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { forwardRef, useMemo } from 'react';
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
ColorInput as MantineColorInput,
|
|
37
|
+
type ColorInputProps as MantineColorInputProps,
|
|
38
|
+
} from '@mantine/core';
|
|
39
|
+
|
|
40
|
+
import { AVATAR_PALETTE } from '../../utils/avatar';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default swatch grid for `ColorInput`. Mirrors `AVATAR_PALETTE` and is
|
|
44
|
+
* exported so consumers can compose it with extra brand-specific swatches
|
|
45
|
+
* (`swatches={[...DEFAULT_COLOR_INPUT_SWATCHES, '#abc123']}`).
|
|
46
|
+
*/
|
|
47
|
+
export const DEFAULT_COLOR_INPUT_SWATCHES: readonly string[] = AVATAR_PALETTE;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Default number of swatches per row. 10 columns × 5 rows fits the 50-entry
|
|
51
|
+
* `AVATAR_PALETTE` cleanly without forcing the popover wider than the input.
|
|
52
|
+
*/
|
|
53
|
+
export const DEFAULT_COLOR_INPUT_SWATCHES_PER_ROW = 10;
|
|
54
|
+
|
|
55
|
+
export interface ColorInputProps extends MantineColorInputProps {}
|
|
56
|
+
|
|
57
|
+
export const ColorInput = forwardRef<HTMLInputElement, ColorInputProps>(
|
|
58
|
+
(
|
|
59
|
+
{
|
|
60
|
+
className,
|
|
61
|
+
size = 'sm',
|
|
62
|
+
radius = 'lg',
|
|
63
|
+
format = 'hex',
|
|
64
|
+
swatches,
|
|
65
|
+
swatchesPerRow = DEFAULT_COLOR_INPUT_SWATCHES_PER_ROW,
|
|
66
|
+
...props
|
|
67
|
+
},
|
|
68
|
+
ref,
|
|
69
|
+
) => {
|
|
70
|
+
// `swatches` is mutated by Mantine internals in some flows, so feed it a
|
|
71
|
+
// fresh array (not the frozen palette constant) when defaulting.
|
|
72
|
+
const resolvedSwatches = useMemo(
|
|
73
|
+
() => swatches ?? [...DEFAULT_COLOR_INPUT_SWATCHES],
|
|
74
|
+
[swatches],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<MantineColorInput
|
|
79
|
+
ref={ref}
|
|
80
|
+
size={size}
|
|
81
|
+
radius={radius}
|
|
82
|
+
format={format}
|
|
83
|
+
swatches={resolvedSwatches}
|
|
84
|
+
swatchesPerRow={swatchesPerRow}
|
|
85
|
+
classNames={{
|
|
86
|
+
input: className,
|
|
87
|
+
}}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
ColorInput.displayName = 'ColorInput';
|
|
@@ -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
|
/>
|
|
@@ -36,13 +36,18 @@ export const selectButton = style({
|
|
|
36
36
|
overflow: 'hidden',
|
|
37
37
|
transition: 'border-color 0.15s ease',
|
|
38
38
|
selectors: {
|
|
39
|
-
'&:hover': {
|
|
39
|
+
'&:hover:not(:disabled)': {
|
|
40
40
|
borderColor: tokens.color.stroke.subduedStrong,
|
|
41
41
|
},
|
|
42
|
-
'&:focus': focusRing,
|
|
43
|
-
'&:active': {
|
|
42
|
+
'&:focus:not(:disabled)': focusRing,
|
|
43
|
+
'&:active:not(:disabled)': {
|
|
44
44
|
borderColor: tokens.color.stroke.strong,
|
|
45
45
|
},
|
|
46
|
+
'&:disabled': {
|
|
47
|
+
cursor: 'not-allowed',
|
|
48
|
+
opacity: 0.6,
|
|
49
|
+
backgroundColor: tokens.color.background.disabledDefault,
|
|
50
|
+
},
|
|
46
51
|
[`${mantineVars.darkSelector} &`]: {
|
|
47
52
|
borderColor: tokens.color.stroke.default,
|
|
48
53
|
backgroundColor: tokens.color.background.default,
|
|
@@ -35,6 +35,11 @@ export interface SelectProps {
|
|
|
35
35
|
* Validation error message displayed below the select
|
|
36
36
|
*/
|
|
37
37
|
error?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Disable the select trigger. When disabled, the dropdown cannot be opened
|
|
40
|
+
* and the button is excluded from the focus order.
|
|
41
|
+
*/
|
|
42
|
+
disabled?: boolean;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
/**
|
|
@@ -55,7 +60,10 @@ export interface SelectProps {
|
|
|
55
60
|
* ```
|
|
56
61
|
*/
|
|
57
62
|
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|
58
|
-
(
|
|
63
|
+
(
|
|
64
|
+
{ labelPrefix, data, value, onChange, placeholder, w, error, disabled },
|
|
65
|
+
ref,
|
|
66
|
+
) => {
|
|
59
67
|
const id = useId();
|
|
60
68
|
const errorId = error ? `${id}-error` : undefined;
|
|
61
69
|
const combobox = useCombobox({
|
|
@@ -88,11 +96,13 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|
|
88
96
|
<button
|
|
89
97
|
ref={ref}
|
|
90
98
|
type="button"
|
|
99
|
+
disabled={disabled}
|
|
91
100
|
className={styles.selectButton}
|
|
92
101
|
onClick={() => combobox.toggleDropdown()}
|
|
93
102
|
aria-haspopup="listbox"
|
|
94
103
|
aria-expanded={combobox.dropdownOpened}
|
|
95
104
|
aria-invalid={!!error}
|
|
105
|
+
aria-disabled={disabled || undefined}
|
|
96
106
|
aria-describedby={errorId}
|
|
97
107
|
style={
|
|
98
108
|
w ? { width: typeof w === 'number' ? `${w}px` : w } : undefined
|
|
@@ -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
|
|