@databricks/design-system 2.0.6 → 2.0.8

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 (31) hide show
  1. package/AGENTS.md +239 -166
  2. package/CHANGELOG.md +36 -0
  3. package/dist/{RHFControlledTypeaheadComboboxV2-Ct9x3rC1.js → RHFControlledTypeaheadComboboxV2-Boee19c1.js} +3 -3
  4. package/dist/{RHFControlledTypeaheadComboboxV2-Ct9x3rC1.js.map → RHFControlledTypeaheadComboboxV2-Boee19c1.js.map} +1 -1
  5. package/dist/{WizardStepContentWrapper-6YTjeEaA.js → WizardStepContentWrapper-DfErft8z.js} +201 -257
  6. package/dist/WizardStepContentWrapper-DfErft8z.js.map +1 -0
  7. package/dist/dubois-colors.less +3 -1
  8. package/dist/icon-metadata.json +5 -0
  9. package/dist/{index-DiRpSwH2.js → index-CUzS-Vjx.js} +121 -109
  10. package/dist/index-CUzS-Vjx.js.map +1 -0
  11. package/dist/index-dark.css +136 -1
  12. package/dist/index-dark.mitigated.css +164 -16
  13. package/dist/index.css +183 -28
  14. package/dist/index.js +2 -2
  15. package/dist/index.mitigated.css +211 -43
  16. package/dist/patterns.js +1 -1
  17. package/dist-types/design-system/Alert/Alert.d.ts +21 -4
  18. package/dist-types/design-system/Button/Button.d.ts +2 -1
  19. package/dist-types/design-system/Icon/__generated/icons/SlidesIcon.d.ts +4 -0
  20. package/dist-types/design-system/Icon/__generated/icons/index.d.ts +1 -0
  21. package/dist-types/design-system/TypeaheadCombobox/TypeaheadComboboxControls.d.ts +0 -2
  22. package/dist-types/design-system/index.d.ts +0 -1
  23. package/dist-types/design-system/utils/safex.d.ts +4 -2
  24. package/dist-types/theme/_generated/ValidSemanticColors.d.ts +1 -0
  25. package/dist-types/theme/generalVariables.d.ts +0 -1
  26. package/package.json +5 -4
  27. package/setup.mjs +586 -0
  28. package/dist/WizardStepContentWrapper-6YTjeEaA.js.map +0 -1
  29. package/dist/index-DiRpSwH2.js.map +0 -1
  30. package/dist-types/design-system/LegacyTooltip/LegacyTooltip.d.ts +0 -47
  31. package/dist-types/design-system/LegacyTooltip/index.d.ts +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@databricks/design-system",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "DuBois Design System",
5
5
  "keywords": [],
6
6
  "license": "ISC",
@@ -55,6 +55,7 @@
55
55
  "files": [
56
56
  "/AGENTS.md",
57
57
  "/CHANGELOG.md",
58
+ "/setup.mjs",
58
59
  "dist",
59
60
  "dist-types",
60
61
  "test-utils",
@@ -106,7 +107,7 @@
106
107
  "@figma/code-connect": "^2.0.0",
107
108
  "@jest/globals": "^30.0.2",
108
109
  "@svgr/cli": "^8.1.0",
109
- "@swc/core": "^1.15.47",
110
+ "@swc/core": "^1.16.2",
110
111
  "@swc/register": "^0.1.10",
111
112
  "@tanstack/react-table": "8.21.2",
112
113
  "@testing-library/jest-dom": "^6.4.2",
@@ -142,7 +143,6 @@
142
143
  },
143
144
  "dependencies": {
144
145
  "@ant-design/icons": "^4.7.0",
145
- "@databricks/testing-library": "1.0.0",
146
146
  "@emotion/unitless": "^0.8.1",
147
147
  "@floating-ui/dom": "^1.6.12",
148
148
  "@floating-ui/react": "^0.26.25",
@@ -186,5 +186,6 @@
186
186
  "react-resizable": "^3.0.4",
187
187
  "tabbable": "^6.4.0",
188
188
  "tslib": "^2.8.1"
189
- }
189
+ },
190
+ "bin": "./setup.mjs"
190
191
  }
package/setup.mjs ADDED
@@ -0,0 +1,586 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @fileoverview `npx @databricks/design-system` — interactive setup for consumers of the dbui
5
+ * design system. Runs in the user's OWN project, outside the universe monorepo, so it uses only
6
+ * Node built-ins and shells out to the canonical scaffolders (create-vite, create-next-app)
7
+ * rather than shipping its own project templates. The wiring it applies (CSS imports, provider,
8
+ * Emotion JSX runtime, css.d.ts, body font) mirrors the shipped AGENTS.md "Setup" section, which
9
+ * is the single source of truth for how a consumer wires up the package.
10
+ *
11
+ * Shipped as plain ESM JavaScript, not TypeScript: Node refuses type-stripping for files under
12
+ * node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING) — which is exactly where npx runs a
13
+ * published bin — so a `.ts`/`.mts` bin would fail to execute.
14
+ */
15
+
16
+ import * as fs from 'node:fs';
17
+ import * as path from 'node:path';
18
+ import * as readline from 'node:readline';
19
+ import { spawnSync } from 'node:child_process';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const PACKAGE_NAME = '@databricks/design-system';
23
+
24
+ // Pin the scaffolders instead of `@latest`: the download must be reviewed and reproducible
25
+ // (Databricks supply-chain policy), not a floating fetch of whatever is newest.
26
+ const VITE_SCAFFOLDER = 'create-vite@9.2.1';
27
+ // create-next-app@14 (Next 14) so the project runs on React 18. dbui 2.0.x is built on AntD 4 and
28
+ // only type-checks under React 18; Next 15+ requires React 19, whose types break AntD-derived
29
+ // props (e.g. Typography.Title). The Vite path pins React 18 explicitly (see REACT_18_PINS).
30
+ const NEXT_SCAFFOLDER = 'create-next-app@14.2.35';
31
+
32
+ // The Vite scaffolder installs React 19, but dbui 2.0.x is typed for React 18 — under React 19's
33
+ // types AntD-derived props resolve as required and `npm run build` fails. Pin the new Vite project
34
+ // back to React 18 so it type-checks.
35
+ const REACT_18_PINS = ['react@^18', 'react-dom@^18', '@types/react@^18', '@types/react-dom@^18'];
36
+
37
+ // Peers to install on demand when a target project is missing them. react/react-dom are
38
+ // deliberately excluded: new projects get React 18 from pinReact18 / create-next-app@14, and an
39
+ // existing project's React is never touched — adding only the missing half of the react/react-dom
40
+ // pair could leave an incompatible version combination.
41
+ const PEER_RANGES = {
42
+ '@emotion/react': '^11.11.3',
43
+ moment: '^2.25.3',
44
+ };
45
+
46
+ // The exact side-effect imports a consumer needs at their entry point (from AGENTS.md).
47
+ const CSS_IMPORTS = [`import '${PACKAGE_NAME}/index.css';`, `import '${PACKAGE_NAME}/fonts/dm-sans.css';`];
48
+
49
+ // ApplyGlobalStyles sets the body background and text color but NOT font-family (per AGENTS.md),
50
+ // so the app must apply the DM Sans token itself.
51
+ const BODY_FONT_CSS = 'body {\n font-family: var(--db-font-family);\n}\n';
52
+
53
+ // TypeScript rejects the CSS side-effect imports without an ambient declaration; AGENTS.md
54
+ // prescribes this one-liner instead of per-import suppressions.
55
+ const CSS_DECLARATION = "declare module '*.css';\n";
56
+
57
+ // The version this script shipped inside. Pinning the scaffolded dependency to it keeps a
58
+ // generated project on the same version the user invoked, rather than drifting to a newer
59
+ // `latest` on their next install. Falls back to `latest` if the manifest can't be read.
60
+ function designSystemSpec() {
61
+ try {
62
+ // fileURLToPath(import.meta.url), not import.meta.dirname, so pinning works on every ESM Node
63
+ // (import.meta.dirname is undefined before Node 20.11, which would fall back to @latest).
64
+ const manifestPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'package.json');
65
+ const { version } = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
66
+ // The in-repo placeholder (0.0.1-0) is never what a real consumer has; only pin a
67
+ // published-looking version.
68
+ if (version && version !== '0.0.1-0') return `${PACKAGE_NAME}@${version}`;
69
+ } catch {
70
+ // fall through
71
+ }
72
+ return `${PACKAGE_NAME}@latest`;
73
+ }
74
+
75
+ // --mode/--name make the scaffold modes non-interactive (used by tests); omit them and the CLI
76
+ // prompts. There is no --yes: the CLI has no confirmation prompts to skip.
77
+ function parseArgs(argv) {
78
+ const args = { mode: undefined, name: undefined };
79
+ for (let i = 0; i < argv.length; i++) {
80
+ const arg = argv[i];
81
+ if (arg === '--mode') args.mode = argv[++i];
82
+ else if (arg === '--name') args.name = argv[++i];
83
+ else if (!arg.startsWith('-') && !args.name) args.name = arg;
84
+ }
85
+ return args;
86
+ }
87
+
88
+ function ask(rl, question) {
89
+ return new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
90
+ }
91
+
92
+ // Runs a command with inherited stdio so the canonical scaffolder's own prompts/output reach the
93
+ // user directly. Exits the process on failure rather than half-finishing a setup.
94
+ function run(command, commandArgs, cwd) {
95
+ const result = spawnSync(command, commandArgs, { cwd, stdio: 'inherit', shell: false });
96
+ if (result.status !== 0) {
97
+ fail(`\`${command} ${commandArgs.join(' ')}\` failed (exit ${result.status}).`);
98
+ }
99
+ }
100
+
101
+ function fail(message) {
102
+ console.error(`\nERROR: ${message}`);
103
+ process.exit(1);
104
+ }
105
+
106
+ function readPackageJson(dir) {
107
+ try {
108
+ return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
109
+ } catch {
110
+ return undefined;
111
+ }
112
+ }
113
+
114
+ function allDeps(dir) {
115
+ const pkg = readPackageJson(dir);
116
+ if (!pkg) return {};
117
+ return { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
118
+ }
119
+
120
+ function hasAnyDep(pkg, names) {
121
+ const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
122
+ return names.some((name) => name in deps);
123
+ }
124
+
125
+ // Picks the package manager from the lockfile a scaffolder or the user already committed to, so
126
+ // we don't cross yarn/pnpm/npm and produce a second lockfile.
127
+ function detectPackageManager(dir) {
128
+ if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm';
129
+ if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn';
130
+ return 'npm';
131
+ }
132
+
133
+ function installCommand(pm, packages) {
134
+ const verb = pm === 'npm' ? 'install' : 'add';
135
+ return [pm, [verb, ...packages]];
136
+ }
137
+
138
+ // Classifies an existing project so "add to existing" applies the right wiring. Order matters: a
139
+ // Next.js project also has react, so check the most specific signal first.
140
+ function detectExistingProject(dir) {
141
+ const pkg = readPackageJson(dir);
142
+ if (!pkg) return { type: 'unknown', reason: 'no package.json found' };
143
+
144
+ const hasConfig = (names) => names.some((n) => fs.existsSync(path.join(dir, n)));
145
+
146
+ if (hasAnyDep(pkg, ['next']) || hasConfig(['next.config.js', 'next.config.ts', 'next.config.mjs'])) {
147
+ return { type: 'next', pkg };
148
+ }
149
+ if (hasAnyDep(pkg, ['vite']) || hasConfig(['vite.config.ts', 'vite.config.js'])) {
150
+ return { type: 'vite', pkg };
151
+ }
152
+ if (hasAnyDep(pkg, ['react-scripts'])) return { type: 'cra', pkg };
153
+ if (hasAnyDep(pkg, ['react'])) return { type: 'react', pkg };
154
+ return { type: 'unknown', reason: 'no React dependency detected', pkg };
155
+ }
156
+
157
+ // Inserts import lines into a source file. A "use client"/"use server" directive is only honored
158
+ // as the first statement, so if one is present the imports go right after it (even when a BOM or a
159
+ // leading license comment sits above it); otherwise they go at the top. Idempotent: lines already
160
+ // present are skipped, so re-running is a no-op.
161
+ function addImports(filePath, lines) {
162
+ const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
163
+ const missing = lines.filter((line) => !existing.includes(line));
164
+ if (missing.length === 0) return false;
165
+
166
+ const sourceLines = existing.split('\n');
167
+ const directiveIndex = sourceLines.findIndex((line) => /^\s*(['"])use (client|server)\1;?\s*$/.test(line));
168
+ const insertAt = directiveIndex >= 0 ? directiveIndex + 1 : 0;
169
+ sourceLines.splice(insertAt, 0, ...missing);
170
+ fs.writeFileSync(filePath, sourceLines.join('\n'));
171
+ return true;
172
+ }
173
+
174
+ function writeFileEnsuringDir(filePath, content) {
175
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
176
+ fs.writeFileSync(filePath, content);
177
+ }
178
+
179
+ // A single ambient `declare module '*.css'` so TypeScript accepts the CSS side-effect imports,
180
+ // placed where the project's tsconfig actually includes it (under src/ for Vite/CRA; the repo
181
+ // root is in scope for Next). Never overwrites an existing declaration file.
182
+ function ensureCssDeclaration(dir, type) {
183
+ const rel = type === 'next' ? 'css.d.ts' : path.join('src', 'css.d.ts');
184
+ const full = path.join(dir, rel);
185
+ if (fs.existsSync(full)) return;
186
+ writeFileEnsuringDir(full, CSS_DECLARATION);
187
+ }
188
+
189
+ // Emotion's `css` prop needs TypeScript itself to use Emotion's JSX types; the bundler/compiler
190
+ // settings only affect the runtime transform, not type-checking. Set jsxImportSource in the
191
+ // project's tsconfig too (per AGENTS.md). Inserts into the first compilerOptions block; skips if
192
+ // the option is already present or the file is missing.
193
+ function addTsconfigJsxImportSource(tsconfigPath) {
194
+ if (!fs.existsSync(tsconfigPath)) return;
195
+ const content = fs.readFileSync(tsconfigPath, 'utf8');
196
+ if (content.includes('jsxImportSource')) return;
197
+ const marker = '"compilerOptions": {';
198
+ if (!content.includes(marker)) return;
199
+ fs.writeFileSync(tsconfigPath, content.replace(marker, `${marker}\n "jsxImportSource": "@emotion/react",`));
200
+ }
201
+
202
+ // Forces a newly scaffolded project onto React 18 (runtime + types), because dbui 2.0.x is typed
203
+ // for React 18. Only for new projects we generate — an existing project's React is never touched.
204
+ function pinReact18(dir, pm) {
205
+ console.log('\n> Pinning React 18 (dbui is typed for React 18)…\n');
206
+ const [command, commandArgs] = installCommand(pm, REACT_18_PINS);
207
+ run(command, commandArgs, dir);
208
+ }
209
+
210
+ // Installs the pinned design-system version plus any PEER_RANGES peer the project is missing
211
+ // (currently @emotion/react and moment). react/react-dom are handled elsewhere — see PEER_RANGES.
212
+ function installDesignSystem(dir, pm) {
213
+ const present = allDeps(dir);
214
+ const missingPeers = Object.entries(PEER_RANGES)
215
+ .filter(([name]) => !(name in present))
216
+ .map(([name, range]) => `${name}@${range}`);
217
+ const packages = [designSystemSpec(), ...missingPeers];
218
+ console.log(`\n> Installing ${packages.join(', ')} with ${pm}…\n`);
219
+ const [command, commandArgs] = installCommand(pm, packages);
220
+ run(command, commandArgs, dir);
221
+ }
222
+
223
+ function scaffoldVite(name) {
224
+ console.log(`\n> Scaffolding a Vite + React + TypeScript project via ${VITE_SCAFFOLDER}…\n`);
225
+ // npx runs the already-`create-`-prefixed package directly (unlike `npm create`, which would
226
+ // prepend a second `create-`), matching how the Next path invokes create-next-app.
227
+ run('npx', [VITE_SCAFFOLDER, name, '--template', 'react-ts']);
228
+
229
+ const dir = path.resolve(process.cwd(), name);
230
+ const pm = detectPackageManager(dir);
231
+ pinReact18(dir, pm);
232
+ installDesignSystem(dir, pm);
233
+
234
+ // Own the entry file wholesale — we generated this project, so there is no user code to
235
+ // preserve, and a known-good main.tsx is cleaner than surgically editing create-vite's.
236
+ writeFileEnsuringDir(
237
+ path.join(dir, 'src', 'main.tsx'),
238
+ [
239
+ ...CSS_IMPORTS,
240
+ "import './dbui.css';",
241
+ '',
242
+ "import { StrictMode } from 'react';",
243
+ "import { createRoot } from 'react-dom/client';",
244
+ "import { ApplyGlobalStyles, DesignSystemProvider } from '@databricks/design-system';",
245
+ "import { App } from './App';",
246
+ '',
247
+ "createRoot(document.getElementById('root')!).render(",
248
+ ' <StrictMode>',
249
+ ' <DesignSystemProvider>',
250
+ ' <ApplyGlobalStyles />',
251
+ ' <App />',
252
+ ' </DesignSystemProvider>',
253
+ ' </StrictMode>,',
254
+ ');',
255
+ '',
256
+ ].join('\n'),
257
+ );
258
+
259
+ writeFileEnsuringDir(path.join(dir, 'src', 'dbui.css'), BODY_FONT_CSS);
260
+ writeFileEnsuringDir(path.join(dir, 'src', 'App.tsx'), viteAppFile());
261
+ ensureCssDeclaration(dir, 'vite');
262
+ configureViteEmotion(dir);
263
+ addTsconfigJsxImportSource(path.join(dir, 'tsconfig.app.json'));
264
+
265
+ printDone(name, pm, 'vite');
266
+ }
267
+
268
+ // create-vite's react plugin drives the JSX transform, so the Emotion `css` prop needs its
269
+ // jsxImportSource option — the runtime half of the Emotion setup (the tsconfig half is separate).
270
+ function configureViteEmotion(dir) {
271
+ writeFileEnsuringDir(
272
+ path.join(dir, 'vite.config.ts'),
273
+ [
274
+ "import { defineConfig } from 'vite';",
275
+ "import react from '@vitejs/plugin-react';",
276
+ '',
277
+ '// jsxImportSource wires up Emotion so the `css` prop dbui components use works.',
278
+ 'export default defineConfig({',
279
+ " plugins: [react({ jsxImportSource: '@emotion/react' })],",
280
+ '});',
281
+ '',
282
+ ].join('\n'),
283
+ );
284
+ }
285
+
286
+ function scaffoldNext(name) {
287
+ console.log(`\n> Scaffolding a Next.js (Pages Router) project via ${NEXT_SCAFFOLDER}…\n`);
288
+ // --no-app selects the Pages Router. dbui is client-only (Emotion + React context) and can't be
289
+ // server-rendered; Pages Router lets us load the whole app through next/dynamic with ssr:false
290
+ // so nothing dbui touches the server. App Router's RSC build evaluates every route (even
291
+ // /_not-found) on the server, which dbui can't survive — hence Pages Router for this prototype.
292
+ run('npx', [
293
+ NEXT_SCAFFOLDER,
294
+ name,
295
+ '--ts',
296
+ '--no-app',
297
+ '--eslint',
298
+ '--no-tailwind',
299
+ '--no-src-dir',
300
+ '--import-alias',
301
+ '@/*',
302
+ '--use-npm',
303
+ '--yes',
304
+ ]);
305
+
306
+ const dir = path.resolve(process.cwd(), name);
307
+ const pm = detectPackageManager(dir);
308
+ installDesignSystem(dir, pm);
309
+
310
+ // Pages Router requires global CSS to be imported from _app; render the page component plainly.
311
+ writeFileEnsuringDir(
312
+ path.join(dir, 'pages', '_app.tsx'),
313
+ [
314
+ ...CSS_IMPORTS,
315
+ "import '../dbui.css';",
316
+ '',
317
+ "import type { AppProps } from 'next/app';",
318
+ '',
319
+ 'export default function App({ Component, pageProps }: AppProps) {',
320
+ ' return <Component {...pageProps} />;',
321
+ '}',
322
+ '',
323
+ ].join('\n'),
324
+ );
325
+
326
+ writeFileEnsuringDir(
327
+ path.join(dir, 'pages', 'index.tsx'),
328
+ [
329
+ "import dynamic from 'next/dynamic';",
330
+ '',
331
+ '// ssr:false renders dbui only on the client, keeping it out of the build-time prerender.',
332
+ "const DbuiApp = dynamic(() => import('../dbui-app'), { ssr: false });",
333
+ '',
334
+ 'export default function Home() {',
335
+ ' return <DbuiApp />;',
336
+ '}',
337
+ '',
338
+ ].join('\n'),
339
+ );
340
+
341
+ writeFileEnsuringDir(path.join(dir, 'dbui-app.tsx'), nextDbuiAppFile());
342
+ writeFileEnsuringDir(path.join(dir, 'dbui.css'), BODY_FONT_CSS);
343
+ ensureCssDeclaration(dir, 'next');
344
+ configureNextEmotion(dir);
345
+ addTsconfigJsxImportSource(path.join(dir, 'tsconfig.json'));
346
+ writeVercelReadme(dir, name);
347
+
348
+ printDone(name, pm, 'next');
349
+ }
350
+
351
+ // Next's SWC compiler has first-class Emotion support; enabling it is the idiomatic Next way to
352
+ // get the `css` prop working. create-next-app may emit next.config.ts | .mjs | .js, so write our
353
+ // config into whichever it created rather than adding a second, conflicting config file.
354
+ function configureNextEmotion(dir) {
355
+ const existing = ['next.config.ts', 'next.config.mjs', 'next.config.js']
356
+ .map((name) => path.join(dir, name))
357
+ .find((configPath) => fs.existsSync(configPath));
358
+ const target = existing ?? path.join(dir, 'next.config.mjs');
359
+ // Match the export style to the file's module format: a .js config is CommonJS (Next's default
360
+ // unless the project opts into ESM), while .mjs/.ts are ESM. Writing the wrong one makes Next
361
+ // fail to load the config.
362
+ const exportLine = target.endsWith('.js') ? 'module.exports = nextConfig;' : 'export default nextConfig;';
363
+ writeFileEnsuringDir(
364
+ target,
365
+ [
366
+ "/** @type {import('next').NextConfig} */",
367
+ 'const nextConfig = {',
368
+ ' // Enables the Emotion `css` prop that dbui components rely on.',
369
+ ' compiler: { emotion: true },',
370
+ '};',
371
+ '',
372
+ exportLine,
373
+ '',
374
+ ].join('\n'),
375
+ );
376
+ }
377
+
378
+ // Next.js deploys to Vercel with zero config, so we only leave instructions — no vercel.json is
379
+ // needed, and per the setup we never log in, upload, or deploy anything.
380
+ function writeVercelReadme(dir, name) {
381
+ writeFileEnsuringDir(
382
+ path.join(dir, 'DEPLOY.md'),
383
+ [
384
+ `# Deploying ${name} to Vercel`,
385
+ '',
386
+ 'This is a client-only Next.js app, ready to host on Vercel with no extra configuration.',
387
+ '',
388
+ '1. Push this project to a Git repository (GitHub/GitLab/Bitbucket).',
389
+ '2. Import the repo at https://vercel.com/new — Vercel auto-detects Next.js.',
390
+ '',
391
+ 'Or deploy straight from this directory with the Vercel CLI:',
392
+ '',
393
+ '```bash',
394
+ 'npx vercel',
395
+ '```',
396
+ '',
397
+ ].join('\n'),
398
+ );
399
+ }
400
+
401
+ function addToExisting(dir) {
402
+ const detected = detectExistingProject(dir);
403
+ console.log(`\n> Detected project type: ${detected.type}${detected.reason ? ` (${detected.reason})` : ''}`);
404
+
405
+ if (detected.type === 'unknown') {
406
+ console.log('\nCould not identify a React project here. Set it up manually:');
407
+ printManualInstructions('generic');
408
+ return;
409
+ }
410
+
411
+ const pm = detectPackageManager(dir);
412
+ installDesignSystem(dir, pm);
413
+ ensureCssDeclaration(dir, detected.type);
414
+
415
+ // Wiring the provider into arbitrary existing app code is fragile, so we make only the safe,
416
+ // idempotent edit (adding the CSS imports where the entry is unambiguous) and print the
417
+ // provider/font/Emotion steps for the user to apply, rather than rewriting their render.
418
+ const entry = existingEntryFile(dir, detected.type);
419
+ if (entry && addImports(entry, CSS_IMPORTS)) {
420
+ console.log(`> Added the dbui CSS imports to ${path.relative(dir, entry)}`);
421
+ }
422
+ printManualInstructions(detected.type);
423
+ }
424
+
425
+ // Best-effort entry file per framework; undefined when we can't confidently name one.
426
+ function existingEntryFile(dir, type) {
427
+ const candidatesByType = {
428
+ next: ['app/layout.tsx', 'app/layout.jsx', 'app/layout.js', 'pages/_app.tsx', 'pages/_app.jsx', 'pages/_app.js'],
429
+ vite: ['src/main.tsx', 'src/main.jsx', 'src/main.ts', 'src/main.js'],
430
+ cra: ['src/index.tsx', 'src/index.jsx', 'src/index.js'],
431
+ react: [
432
+ 'src/main.tsx',
433
+ 'src/index.tsx',
434
+ 'src/main.jsx',
435
+ 'src/index.jsx',
436
+ 'src/main.ts',
437
+ 'src/main.js',
438
+ 'src/index.js',
439
+ ],
440
+ };
441
+ for (const rel of candidatesByType[type] ?? []) {
442
+ const full = path.join(dir, rel);
443
+ if (fs.existsSync(full)) return full;
444
+ }
445
+ return undefined;
446
+ }
447
+
448
+ function printManualInstructions(type) {
449
+ const providerNote =
450
+ type === 'next'
451
+ ? 'dbui is client-only: put DesignSystemProvider + ApplyGlobalStyles in a "use client" component and render it from your entry — app/layout.tsx (App Router) or pages/_app.tsx (Pages Router). For App Router, load it via next/dynamic with { ssr: false }.'
452
+ : 'Wrap your root component in <DesignSystemProvider><ApplyGlobalStyles /> … </DesignSystemProvider>.';
453
+ console.log(
454
+ [
455
+ '',
456
+ 'Finish setup:',
457
+ ` 1. Import the styles at your entry point:`,
458
+ ...CSS_IMPORTS.map((line) => ` ${line}`),
459
+ ` 2. ${providerNote}`,
460
+ ` 3. Set the body font (ApplyGlobalStyles doesn't): body { font-family: var(--db-font-family); }`,
461
+ ` 4. Configure the Emotion JSX runtime (jsxImportSource: '@emotion/react') so the css prop works.`,
462
+ '',
463
+ 'See the package AGENTS.md for the full setup reference.',
464
+ '',
465
+ ].join('\n'),
466
+ );
467
+ }
468
+
469
+ function sampleBody() {
470
+ return [
471
+ ' <div style={{ padding: theme.spacing.lg }}>',
472
+ ' <Typography.Title>dbui is set up</Typography.Title>',
473
+ ' <Button componentId="starter.get_started_button" type="primary">',
474
+ ' Get started',
475
+ ' </Button>',
476
+ ' </div>',
477
+ ].join('\n');
478
+ }
479
+
480
+ function viteAppFile() {
481
+ return [
482
+ "import { Button, Typography, useDesignSystemTheme } from '@databricks/design-system';",
483
+ '',
484
+ 'export function App() {',
485
+ ' const { theme } = useDesignSystemTheme();',
486
+ ' return (',
487
+ sampleBody(),
488
+ ' );',
489
+ '}',
490
+ '',
491
+ ].join('\n');
492
+ }
493
+
494
+ // The client-only module Next loads with ssr:false: it owns the DesignSystemProvider and the
495
+ // sample, so all dbui usage lives behind the ssr:false boundary and never touches the server.
496
+ function nextDbuiAppFile() {
497
+ return [
498
+ "import { ApplyGlobalStyles, Button, DesignSystemProvider, Typography, useDesignSystemTheme } from '@databricks/design-system';",
499
+ '',
500
+ 'function Sample() {',
501
+ ' const { theme } = useDesignSystemTheme();',
502
+ ' return (',
503
+ sampleBody(),
504
+ ' );',
505
+ '}',
506
+ '',
507
+ 'export default function DbuiApp() {',
508
+ ' return (',
509
+ ' <DesignSystemProvider>',
510
+ ' <ApplyGlobalStyles />',
511
+ ' <Sample />',
512
+ ' </DesignSystemProvider>',
513
+ ' );',
514
+ '}',
515
+ '',
516
+ ].join('\n');
517
+ }
518
+
519
+ function printDone(name, pm, type) {
520
+ const runDev = pm === 'npm' ? 'npm run dev' : `${pm} dev`;
521
+ console.log(
522
+ [
523
+ '',
524
+ `✓ ${name} is ready with @databricks/design-system.`,
525
+ '',
526
+ 'Next steps:',
527
+ ` cd ${name}`,
528
+ ` ${runDev}`,
529
+ ...(type === 'next' ? ['', 'To host it: see DEPLOY.md (Vercel).'] : []),
530
+ '',
531
+ ].join('\n'),
532
+ );
533
+ }
534
+
535
+ async function promptMode(rl) {
536
+ console.log('What would you like to do?\n');
537
+ console.log(' 1) Scaffold a new Vite project (no hosting)');
538
+ console.log(' 2) Scaffold a new Next.js project (Vercel hosting)');
539
+ console.log(' 3) Add @databricks/design-system to an existing project\n');
540
+ const answer = await ask(rl, 'Enter 1, 2, or 3: ');
541
+ return { 1: 'vite', 2: 'next', 3: 'existing' }[answer];
542
+ }
543
+
544
+ async function main() {
545
+ const args = parseArgs(process.argv.slice(2));
546
+
547
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
548
+ try {
549
+ const mode = args.mode ?? (await promptMode(rl));
550
+ if (!mode || !['vite', 'next', 'existing'].includes(mode)) fail('Please choose 1, 2, or 3.');
551
+
552
+ if (mode === 'existing') {
553
+ addToExisting(process.cwd());
554
+ return;
555
+ }
556
+
557
+ const name = args.name || (await ask(rl, `\nProject name (new directory): `));
558
+ if (!name) fail('A project name is required.');
559
+ if (fs.existsSync(path.resolve(process.cwd(), name))) {
560
+ fail(`\`${name}\` already exists here. Choose a name that isn't taken.`);
561
+ }
562
+
563
+ if (mode === 'vite') scaffoldVite(name);
564
+ else scaffoldNext(name);
565
+ } finally {
566
+ rl.close();
567
+ }
568
+ }
569
+
570
+ // A bin is symlinked into node_modules/.bin, so compare realpaths (not the raw argv path) to tell
571
+ // direct execution from being imported by a test.
572
+ const invokedDirectly = Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
573
+ if (invokedDirectly) {
574
+ void main();
575
+ }
576
+
577
+ // Exported for unit testing the network-free logic (detection, wiring, arg parsing).
578
+ export {
579
+ parseArgs,
580
+ detectPackageManager,
581
+ detectExistingProject,
582
+ existingEntryFile,
583
+ addImports,
584
+ designSystemSpec,
585
+ CSS_IMPORTS,
586
+ };