@webjsdev/ui 0.3.10 → 0.3.12

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 (38) hide show
  1. package/README.md +26 -15
  2. package/package.json +3 -3
  3. package/packages/registry/README.md +16 -10
  4. package/packages/registry/components/accordion.ts +18 -0
  5. package/packages/registry/components/alert-dialog.ts +203 -22
  6. package/packages/registry/components/aspect-ratio.ts +12 -0
  7. package/packages/registry/components/card.ts +20 -1
  8. package/packages/registry/components/checkbox.ts +45 -3
  9. package/packages/registry/components/collapsible.ts +15 -0
  10. package/packages/registry/components/dialog.ts +205 -22
  11. package/packages/registry/components/dropdown-menu.ts +390 -22
  12. package/packages/registry/components/hover-card.ts +162 -4
  13. package/packages/registry/components/input.ts +34 -2
  14. package/packages/registry/components/kbd.ts +35 -2
  15. package/packages/registry/components/label.ts +21 -0
  16. package/packages/registry/components/native-select.ts +28 -7
  17. package/packages/registry/components/popover.ts +43 -7
  18. package/packages/registry/components/radio-group.ts +37 -9
  19. package/packages/registry/components/sonner.ts +53 -5
  20. package/packages/registry/components/switch.ts +23 -1
  21. package/packages/registry/components/tabs.ts +15 -0
  22. package/packages/registry/components/textarea.ts +21 -2
  23. package/packages/registry/components/toggle-group.ts +81 -8
  24. package/packages/registry/components/toggle.ts +82 -3
  25. package/packages/registry/components/tooltip.ts +57 -0
  26. package/packages/registry/lib/utils.ts +208 -14
  27. package/packages/registry/package.json +1 -1
  28. package/packages/registry/registry.json +1 -1
  29. package/packages/registry/themes/base-colors.js +2 -2
  30. package/src/commands/add.js +52 -2
  31. package/src/commands/diff.js +2 -2
  32. package/src/commands/info.js +2 -5
  33. package/src/commands/init.js +127 -25
  34. package/src/registry/example.js +7 -4
  35. package/src/registry/fetcher.js +10 -2
  36. package/src/registry/local.js +2 -2
  37. package/src/utils/index.js +0 -1
  38. package/src/utils/detect-project.js +0 -63
@@ -1,8 +1,7 @@
1
1
  import { Command } from 'commander';
2
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import prompts from 'prompts';
5
- import { defaultsForProject } from '../utils/detect-project.js';
6
5
  import { writeConfig, CONFIG_FILE } from '../utils/get-config.js';
7
6
  import { logger } from '../utils/logger.js';
8
7
  import { getRegistryItem, DEFAULT_REGISTRY_URL } from '../registry/fetcher.js';
@@ -10,22 +9,96 @@ import { ensureTheme } from '../utils/theme.js';
10
9
 
11
10
  const BASE_COLORS = ['neutral', 'stone', 'zinc', 'mauve', 'olive', 'mist', 'taupe'];
12
11
 
12
+ /**
13
+ * The stylesheet `init` defaults to. It is `styles/globals.css`, NOT
14
+ * `app/globals.css`, because in WebJs `app/` is routing-only, so a
15
+ * non-routing stylesheet lives outside it.
16
+ *
17
+ * Exported so `webjs create` can be checked against it rather than against a
18
+ * hand-copied literal (`test/scaffolds/scaffold-ui-integration.test.js`).
19
+ */
20
+ export const DEFAULT_TAILWIND_CSS = 'styles/globals.css';
21
+
22
+ /**
23
+ * The alias map `init` writes into `components.json`. These are the paths a
24
+ * WebJs app uses, and they match byte for byte what `webjs create` scaffolds
25
+ * (see `packages/cli/lib/create.js`), so `init` on a bare app and a freshly
26
+ * scaffolded one land on the same layout.
27
+ *
28
+ * `utils` is `lib/utils/cn`, not `lib/utils`: `get-config.js` appends `'.ts'`
29
+ * when resolving the alias, so this resolves to `lib/utils/cn.ts`. Writing
30
+ * the helper into the `lib/utils/` DIRECTORY is what lets the client-only
31
+ * `onBeforeCache()` helper sit beside it at `lib/utils/dom.ts` (#819), which
32
+ * is where `add` rewrites the registry's `'../lib/dom.ts'` import to point.
33
+ *
34
+ * Exported for the same drift check as {@link DEFAULT_TAILWIND_CSS}. The two
35
+ * generators used to disagree here (init said `lib/utils`, the scaffold said
36
+ * `lib/utils/cn`) with only a comment claiming they matched, so the agreement
37
+ * is asserted now rather than described.
38
+ */
39
+ export const DEFAULT_ALIASES = {
40
+ components: 'components',
41
+ utils: 'lib/utils/cn',
42
+ ui: 'components/ui',
43
+ lib: 'lib',
44
+ };
45
+
13
46
  export const init = new Command()
14
47
  .name('init')
15
- .description('Initialize @webjsdev/ui in a project: writes components.json, theme CSS, lib/utils')
48
+ .description('Initialize @webjsdev/ui in a project: writes components.json, theme CSS, lib/utils/cn.ts')
16
49
  .option('-c, --cwd <cwd>', 'the working directory', process.cwd())
17
50
  .option('-y, --yes', 'skip confirmation prompts', false)
18
51
  .option('--base-color <color>', `base color (${BASE_COLORS.join('|')})`)
19
52
  .option('--css <path>', 'path to the project Tailwind CSS file')
53
+ .option('-o, --overwrite', 'replace an existing alias map and existing helper files', false)
20
54
  .option('--registry <url>', 'registry base URL', DEFAULT_REGISTRY_URL)
21
55
  .action(async (opts) => {
22
56
  const cwd = opts.cwd;
23
- const defaults = defaultsForProject(cwd);
57
+
58
+ // Re-running `init` on a project that already has one must not RELOCATE it.
59
+ // The alias map decides where cn() lives and what every already-added
60
+ // component imports, and `tailwind.css` decides which stylesheet the app
61
+ // actually builds, so adopting the current defaults over an existing
62
+ // project's values would strand those imports and append the tokens to a
63
+ // file nothing compiles. Keep what the project declared; `--overwrite`
64
+ // opts into replacing it.
65
+ //
66
+ // Read the RAW json rather than going through `getConfig`: its schema is
67
+ // `.strict()`, so a config carrying any unknown key (a shadcn-shaped one
68
+ // with `rsc` / `tsx`, or a hand-added field) throws, and treating that as
69
+ // "no config" is exactly what would relocate a real project and then
70
+ // overwrite the file. Only a file we cannot parse AT ALL is a refusal,
71
+ // because silently replacing unreadable config is the same data loss.
72
+ const cfgPath = join(cwd, CONFIG_FILE);
73
+ /** @type {any} */
74
+ let existing = null;
75
+ if (existsSync(cfgPath)) {
76
+ try {
77
+ existing = JSON.parse(readFileSync(cfgPath, 'utf8'));
78
+ } catch {
79
+ if (!opts.overwrite) {
80
+ logger.error(`${CONFIG_FILE} exists but is not valid JSON, so its settings cannot be preserved.`);
81
+ logger.info('Fix or delete it, or re-run with --overwrite to replace it.');
82
+ process.exit(1);
83
+ }
84
+ }
85
+ }
86
+ const keep = existing && !opts.overwrite;
87
+ if (keep) {
88
+ logger.info(`${CONFIG_FILE} already exists: keeping its settings (use --overwrite to reset them).`);
89
+ }
90
+ // Layer over the defaults rather than replacing them: the schema gives
91
+ // several alias keys defaults, so a hand-written map may legitimately omit
92
+ // one, and reading it raw (above) skips that default-filling. Taking the
93
+ // map wholesale left `utils` undefined and crashed on the write.
94
+ const aliases = { ...DEFAULT_ALIASES, ...((keep && existing.aliases) || null) };
95
+ const initialCss = (keep && existing.tailwind && existing.tailwind.css) || DEFAULT_TAILWIND_CSS;
96
+ const initialBaseColor = (keep && existing.tailwind && existing.tailwind.baseColor) || 'neutral';
24
97
 
25
98
  /** @type {{ baseColor: string, css: string }} */
26
99
  let answers = {
27
- baseColor: opts.baseColor || 'neutral',
28
- css: opts.css || defaults.tailwindCss,
100
+ baseColor: opts.baseColor || initialBaseColor,
101
+ css: opts.css || initialCss,
29
102
  };
30
103
 
31
104
  if (!opts.yes) {
@@ -36,13 +109,13 @@ export const init = new Command()
36
109
  name: 'baseColor',
37
110
  message: 'Base color?',
38
111
  choices: BASE_COLORS.map((c) => ({ title: c, value: c })),
39
- initial: 0,
112
+ initial: Math.max(0, BASE_COLORS.indexOf(initialBaseColor)),
40
113
  },
41
114
  {
42
115
  type: opts.css ? null : 'text',
43
116
  name: 'css',
44
117
  message: 'Tailwind CSS file path?',
45
- initial: defaults.tailwindCss,
118
+ initial: initialCss,
46
119
  },
47
120
  ],
48
121
  { onCancel: () => process.exit(1) },
@@ -50,23 +123,29 @@ export const init = new Command()
50
123
  answers = { ...answers, ...r };
51
124
  }
52
125
 
126
+ // Built from KNOWN keys only. The schema is `.strict()`, so carrying an
127
+ // unrecognised key through here would write a file that `getConfig` (and
128
+ // therefore `add` / `diff` / `info`) throws on. Preserving a project's
129
+ // settings means its VALUES, not arbitrary extra fields.
53
130
  const config = {
54
131
  $schema: 'https://ui.webjs.dev/schema.json',
55
- style: 'default',
132
+ style: (keep && existing.style) || 'default',
56
133
  tailwind: {
57
134
  css: answers.css,
58
135
  baseColor: answers.baseColor,
59
- cssVariables: true,
136
+ cssVariables: keep && existing.tailwind && existing.tailwind.cssVariables !== undefined
137
+ ? existing.tailwind.cssVariables
138
+ : true,
60
139
  },
61
- aliases: defaults.aliases,
62
- iconLibrary: 'lucide',
140
+ aliases,
141
+ iconLibrary: (keep && existing.iconLibrary) || 'lucide',
63
142
  };
64
143
 
65
144
  writeConfig(cwd, config);
66
145
  logger.success(`Wrote ${CONFIG_FILE}`);
67
146
 
68
147
  // Pull lib/utils + the chosen theme from the registry and write them in.
69
- await writeLibUtils(cwd, defaults.aliases.utils, opts.registry);
148
+ await writeLibUtils(cwd, aliases.utils, opts.registry, opts.overwrite);
70
149
 
71
150
  // The theme tokens are what the class helpers render against. A silent
72
151
  // failure here (the old behaviour) left an unstyled install with a clean
@@ -83,33 +162,56 @@ export const init = new Command()
83
162
  logger.break();
84
163
  logger.success('Done.');
85
164
  logger.info('');
86
- logger.info(`Add components with: ${logger.cyan('npx webjsui add button card dialog')}`);
165
+ logger.info(`Add components with: ${logger.cyan('npx @webjsdev/ui add button card dialog')}`);
87
166
  });
88
167
 
89
- async function writeLibUtils(cwd, utilsAlias, registryUrl) {
90
- // `utils` alias points at e.g. "lib/utils" so we write to lib/utils.ts
168
+ /**
169
+ * Write the two shared helpers, WITHOUT clobbering a copy the project already
170
+ * has. The kit is source-copied and you-own-it, so `lib/utils/cn.ts` is a file
171
+ * users are told to edit ("change one helper to retune the entire app"), and
172
+ * `webjs create` puts its own copy there. An unguarded write was harmless while
173
+ * init targeted `lib/utils.ts`, a path nothing else owned, but the moment it
174
+ * writes where the scaffold and the user's edits live it becomes silent data
175
+ * loss, and re-running init is the documented fix for an unstyled install. So
176
+ * an existing file is kept unless `--overwrite` says otherwise. `add` keeps
177
+ * the same two files on the same terms (its `--yes` skips prompts but does not
178
+ * license replacing them), and the theme is idempotent, so every path into a
179
+ * project leaves an edited helper alone.
180
+ */
181
+ async function writeLibUtils(cwd, utilsAlias, registryUrl, overwrite) {
182
+ // The `utils` alias omits the extension ("lib/utils/cn"), matching how
183
+ // get-config.js resolves it, so append '.ts' to get the target path.
91
184
  const utilsRel = utilsAlias.replace(/^@\//, '') + '.ts';
92
185
  const utilsTarget = join(cwd, utilsRel);
93
186
  try {
94
- const item = await getRegistryItem('lib-utils', registryUrl);
95
- if (item.files) {
96
- for (const f of item.files) {
97
- ensureDir(dirname(utilsTarget));
98
- writeFileSync(utilsTarget, f.content || '', 'utf8');
99
- logger.success(`Wrote ${utilsAlias}.ts`);
187
+ if (existsSync(utilsTarget) && !overwrite) {
188
+ logger.info(`${utilsRel} already exists: keeping it.`);
189
+ } else {
190
+ const item = await getRegistryItem('lib-utils', registryUrl);
191
+ if (item.files) {
192
+ for (const f of item.files) {
193
+ ensureDir(dirname(utilsTarget));
194
+ writeFileSync(utilsTarget, f.content || '', 'utf8');
195
+ logger.success(`Wrote ${utilsRel}`);
196
+ }
100
197
  }
101
198
  }
102
199
  } catch (e) {
103
- logger.warn(`Could not fetch lib-utils from registry (${e.message}). You may need to write lib/utils.ts manually.`);
200
+ logger.warn(`Could not fetch lib-utils from registry (${e.message}). You may need to write ${utilsRel} manually.`);
104
201
  }
105
202
 
106
203
  // The onBeforeCache() DOM helper lives in a SEPARATE module (#819) because
107
204
  // it references `document`, so keeping it out of cn()'s file prevents the
108
205
  // elision analyzer pinning every page that imports cn to the browser.
109
206
  // Overlay components import it from `../lib/dom.ts`, so write it as a
110
- // sibling of the utils file (e.g. lib/utils/dom.ts next to cn.ts).
207
+ // sibling of the utils file, which lands it at lib/utils/dom.ts next to
208
+ // cn.ts. `add` reads that same adjacency when it rewrites the import.
111
209
  const domTarget = join(dirname(utilsTarget), 'dom.ts');
112
210
  try {
211
+ if (existsSync(domTarget) && !overwrite) {
212
+ logger.info(`${relative(cwd, domTarget)} already exists: keeping it.`);
213
+ return;
214
+ }
113
215
  const item = await getRegistryItem('lib-dom', registryUrl);
114
216
  if (!item.files) return;
115
217
  for (const f of item.files) {
@@ -118,7 +220,7 @@ async function writeLibUtils(cwd, utilsAlias, registryUrl) {
118
220
  logger.success(`Wrote ${relative(cwd, domTarget)}`);
119
221
  }
120
222
  } catch (e) {
121
- logger.warn(`Could not fetch lib-dom from registry (${e.message}). You may need to write lib/dom.ts manually.`);
223
+ logger.warn(`Could not fetch lib-dom from registry (${e.message}). You may need to write ${relative(cwd, domTarget)} manually.`);
122
224
  }
123
225
  }
124
226
 
@@ -79,10 +79,13 @@ export function extractExample(src) {
79
79
 
80
80
  /**
81
81
  * The one-line pointer left in place of a stripped example. Uses the EXPLICIT
82
- * `npx @webjsdev/ui view` form (not the bare `webjsui` bin name, which npx would
83
- * resolve to an unrelated `webjsui` package when it is not a direct dep, e.g. in
84
- * a scaffolded app where `@webjsdev/ui` is only transitive). Works in a WebJs
85
- * app and a standalone project alike.
82
+ * `npx @webjsdev/ui view` form, not the bare `webjsui` bin name. `webjsui` is a
83
+ * bin declared INSIDE `@webjsdev/ui` and is not a published package name (the
84
+ * registry 404s on it), so `npx webjsui` resolves through a local
85
+ * `node_modules/.bin` link or not at all. This pointer is written into a file
86
+ * the user keeps, and it cannot know whether the project reading it later has
87
+ * the kit in its tree. The explicit form resolves either way. See invariant 8
88
+ * in this package's AGENTS.md for the rule every printed hint follows (#1264).
86
89
  */
87
90
  export function pointerLine(name) {
88
91
  return `Full usage example: npx @webjsdev/ui view ${name} (or the MCP tool: ui ${name})`;
@@ -4,8 +4,16 @@ import { loadRegistryItem, loadRegistryIndex } from './local.js';
4
4
  /**
5
5
  * The canonical hosted registry. Local-first resolution applies ONLY for this
6
6
  * exact URL: it is the one registry whose sources ship inside the package.
7
+ *
8
+ * This moved from `https://ui.webjs.dev/registry` when the gallery merged into
9
+ * the marketing site (#1099). That old URL no longer serves the registry: it
10
+ * now redirects to the gallery PAGE, so a fetch of it follows the redirect and
11
+ * gets HTML. Versions 0.3.1 through 0.3.8 hardcode it and hit the network, so
12
+ * `webjsui add` is broken on those and cannot be corrected after publish.
13
+ * Since 0.3.9 the kit resolves LOCAL-first, which is why the break stops
14
+ * there: `add` / `list` / `view` never reach the network on this URL.
7
15
  */
8
- export const HOSTED_REGISTRY_URL = 'https://ui.webjs.dev/registry';
16
+ export const HOSTED_REGISTRY_URL = 'https://webjs.dev/ui/registry';
9
17
 
10
18
  /**
11
19
  * Default registry URL. A `REGISTRY_URL` env var points at a CUSTOM registry,
@@ -47,7 +55,7 @@ export async function getRegistryItem(name, registryUrl) {
47
55
  const item = loadRegistryItem(name);
48
56
  if (!item) {
49
57
  throw new Error(
50
- `Unknown registry item "${name}". Run \`webjsui list\` to see the available components.`,
58
+ `Unknown registry item "${name}". Run \`npx @webjsdev/ui list\` to see the available components.`,
51
59
  );
52
60
  }
53
61
  return item;
@@ -8,8 +8,8 @@
8
8
  * item shape the hosted registry serves, so a local read and a network fetch
9
9
  * are interchangeable for every consumer.
10
10
  *
11
- * It is the plain-JS twin of the ui-website composer
12
- * (`packages/ui/packages/website/app/_lib/registry.server.ts`): both read
11
+ * It is the plain-JS twin of the website gallery composer
12
+ * (`website/modules/ui/queries/registry.server.ts`): both read
13
13
  * `packages/registry/registry.json` + the `components/*.ts` sources and
14
14
  * synthesize the 6 non-neutral base-colour themes from `themes/base-colors.js`.
15
15
  * Keeping the CLI on this module (rather than always hitting the network) is
@@ -1,3 +1,2 @@
1
1
  export { getConfig, writeConfig, CONFIG_FILE } from './get-config.js';
2
- export { detectProject, defaultsForProject } from './detect-project.js';
3
2
  export { logger } from './logger.js';
@@ -1,63 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
-
4
- /**
5
- * Detect the host project type so we can pick sensible defaults for paths.
6
- *
7
- * @param {string} cwd
8
- * @returns {{ type: 'webjs' | 'next' | 'vite' | 'astro' | 'plain', meta: any }}
9
- */
10
- export function detectProject(cwd = process.cwd()) {
11
- const pkgPath = join(cwd, 'package.json');
12
- if (!existsSync(pkgPath)) return { type: 'plain', meta: {} };
13
-
14
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
15
- const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
16
-
17
- if (deps['@webjsdev/server'] || deps['@webjsdev/cli'] || existsSync(join(cwd, 'app', 'layout.ts')) || existsSync(join(cwd, 'app', 'layout.js'))) {
18
- return { type: 'webjs', meta: { pkg } };
19
- }
20
- if (deps['next']) return { type: 'next', meta: { pkg } };
21
- if (deps['vite'] || deps['@vitejs/plugin-react'] || deps['@vitejs/plugin-vue']) return { type: 'vite', meta: { pkg } };
22
- if (deps['astro']) return { type: 'astro', meta: { pkg } };
23
-
24
- return { type: 'plain', meta: { pkg } };
25
- }
26
-
27
- /**
28
- * Pick default `aliases` + `tailwind.css` path based on the detected project.
29
- *
30
- * @param {string} cwd
31
- */
32
- export function defaultsForProject(cwd = process.cwd()) {
33
- const { type } = detectProject(cwd);
34
- switch (type) {
35
- case 'webjs':
36
- return {
37
- // styles/globals.css, NOT app/globals.css: in webjs, app/ is routing-only,
38
- // so a non-routing stylesheet lives outside it.
39
- tailwindCss: 'styles/globals.css',
40
- aliases: { components: 'components', utils: 'lib/utils', ui: 'components/ui', lib: 'lib' },
41
- };
42
- case 'next':
43
- return {
44
- tailwindCss: 'app/globals.css',
45
- aliases: { components: '@/components', utils: '@/lib/utils', ui: '@/components/ui', lib: '@/lib' },
46
- };
47
- case 'vite':
48
- return {
49
- tailwindCss: 'src/index.css',
50
- aliases: { components: 'src/components', utils: 'src/lib/utils', ui: 'src/components/ui', lib: 'src/lib' },
51
- };
52
- case 'astro':
53
- return {
54
- tailwindCss: 'src/styles/globals.css',
55
- aliases: { components: 'src/components', utils: 'src/lib/utils', ui: 'src/components/ui', lib: 'src/lib' },
56
- };
57
- default:
58
- return {
59
- tailwindCss: 'styles/globals.css',
60
- aliases: { components: 'components', utils: 'lib/utils', ui: 'components/ui', lib: 'lib' },
61
- };
62
- }
63
- }