uix-ui 0.1.1 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +18 -0
  2. package/bin/uix-ui.mjs +100 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -19,4 +19,22 @@ import { Button } from "@/components/ui/button";
19
19
  Dark mode: `<html data-theme="dark">`. Options: `--overwrite` (add), `--no-install` (print the install command
20
20
  instead of running it), `--registry <url|folder>` (init).
21
21
 
22
+ UIX Pro (marketing blocks, nine visual styles, 210 more themes): set your key, then add them by name.
23
+
24
+ ```bash
25
+ export UIX_LICENSE=<your key>
26
+ npx uix-ui@latest add hero pricing-card skins
27
+ npx uix-ui@latest theme acid-prompt
28
+ ```
29
+
30
+ Your own design system, made at [uixdesign.pagestro.com/generate](https://uixdesign.pagestro.com/generate) and saved to
31
+ your account: pull its theme, tokens (DTCG, in `src/styles/tokens`) and any components, then sync when it changes.
32
+ The API key is on your account page. `uix.json` remembers the system and the version you have.
33
+
34
+ ```bash
35
+ export UIX_API_KEY=uixm_…
36
+ npx uix-ui@latest pull <id> button card # the id is in your Studio link: /s/<id>
37
+ npx uix-ui@latest sync # pulls again only if you saved a newer version
38
+ ```
39
+
22
40
  Docs, live components and all 42 themes: https://uixdesign.pagestro.com/ui · Source: https://github.com/mdrakibshekh/uix-ui · MIT
package/bin/uix-ui.mjs CHANGED
@@ -5,17 +5,23 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
5
5
  import { dirname, join, relative, resolve, sep } from 'node:path';
6
6
  import { spawnSync } from 'node:child_process';
7
7
 
8
- const DEFAULT_REGISTRY = 'https://uixdesign.pagestro.com/ui/r';
8
+ const SITE = 'https://uixdesign.pagestro.com';
9
+ const DEFAULT_REGISTRY = `${SITE}/ui/r`;
9
10
  const HELP = `uix-ui: UIX components and themes for React + Tailwind v4
10
11
 
11
12
  npx uix-ui init [--preset <id>] [--registry <url|folder>] set up the theme, utils and components.json
12
13
  npx uix-ui add <component...> copy components into your app
13
14
  npx uix-ui theme <preset> switch to another UIX preset (uix = default)
14
15
  npx uix-ui list components and themes in the registry
16
+ npx uix-ui pull <id> [component...] your saved design system (uixdesign.pagestro.com/s/<id>):
17
+ its theme, tokens and components; remembered in uix.json
18
+ npx uix-ui sync pull again if the system changed since the last pull
15
19
 
16
20
  --overwrite replace files that already exist (add)
17
21
  --no-install don't run the package manager, just print the command
18
- Pro items: set UIX_LICENSE=<your key> and add @uix-pro/<name>.`;
22
+ pull, sync: set UIX_API_KEY (or --key) to the API key from your account page (uixdesign.pagestro.com/account).
23
+ UIX Pro: set UIX_LICENSE=<your key>, then add Pro items by name (hero, pricing-card, skins…) or switch to a
24
+ Pro theme (npx uix-ui theme acid-prompt). \`list\` shows them all.`;
19
25
 
20
26
  const cwd = process.cwd();
21
27
  const argv = process.argv.slice(2);
@@ -26,7 +32,7 @@ const flag = (n, bool = false) => {
26
32
  argv.splice(i, takes ? 2 : 1);
27
33
  return takes ? v : true;
28
34
  };
29
- const opts = { preset: flag('preset'), registry: flag('registry'), overwrite: flag('overwrite', true), noInstall: flag('no-install', true) };
35
+ const opts = { preset: flag('preset'), registry: flag('registry'), overwrite: flag('overwrite', true), noInstall: flag('no-install', true), key: flag('key'), site: flag('site') };
30
36
  const [cmd, ...names] = argv;
31
37
 
32
38
  const say = (s) => console.log(s);
@@ -54,28 +60,57 @@ function source(cfg, name) {
54
60
  return { ns, item, url: url.replace('{name}', item), headers: Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, env(v)])) };
55
61
  }
56
62
 
63
+ const PRICING = 'https://uixdesign.pagestro.com/pricing';
64
+ let pin = ''; // pull: `?v=<version>`, so the site answers with that version, not a copy cached a few seconds ago
65
+ const missing = (src) => Object.assign(new Error(`"${src.item}" is not in the registry (${src.url}).`), { missing: true });
66
+
57
67
  async function load(src) {
58
68
  if (!/^https?:/.test(src.url)) {
59
- if (!existsSync(src.url)) die(`"${src.item}" is not in the registry (${src.url}).`);
69
+ if (!existsSync(src.url)) throw missing(src);
60
70
  return JSON.parse(readFileSync(src.url, 'utf8'));
61
71
  }
62
- const res = await fetch(src.url, { headers: src.headers });
63
- if (res.status === 401 || res.status === 403) die(`"${src.item}" is a UIX Pro item. Set UIX_LICENSE to your key (https://uixdesign.pagestro.com/pricing).`);
72
+ const res = await fetch(src.url + pin, { headers: src.headers });
73
+ if (res.status === 401) die(process.env.UIX_LICENSE ? `UIX_LICENSE is set, but that key wasn't found. Check it in your UIX account (${PRICING}).` : `${src.item.startsWith('theme-') ? `The "${src.item.slice(6)}" theme` : `"${src.item}"`} is part of UIX Pro. Set UIX_LICENSE to your key (${PRICING}).`);
74
+ if (res.status === 403) die(`"${src.item}" needs an active UIX Pro licence; this key's plan has ended (${PRICING}).`);
75
+ if (res.status === 404) throw missing(src);
64
76
  if (!res.ok) die(`"${src.item}" could not be loaded (${res.status} from ${src.url}).`);
65
77
  return res.json();
66
78
  }
67
79
 
80
+ /** The Pro index (names only, no key needed), or null when there is no Pro registry. */
81
+ let proIndex;
82
+ async function loadProIndex(cfg) {
83
+ if (proIndex !== undefined) return proIndex;
84
+ if (!cfg.registries?.['@uix-pro']) return (proIndex = null);
85
+ const ps = source(cfg, '@uix-pro/x');
86
+ return (proIndex = await load({ ...ps, item: 'registry.json', url: ps.url.replace(/\/x\.json$/, '/registry.json'), headers: {} }).catch(() => null));
87
+ }
88
+
89
+ /** A bare name that the free registry doesn't have is looked up in Pro (`hero` → `@uix-pro/hero`). */
90
+ async function loadAny(cfg, src) {
91
+ try { return { src, it: await load(src) }; } catch (e) {
92
+ if (!e.missing || src.ns !== '@uix') throw e;
93
+ const index = await loadProIndex(cfg);
94
+ if (!index?.items.some(i => i.name === src.item)) throw Object.assign(new Error(`"${src.item.replace(/^theme-/, '')}" is not a uix-ui component or theme. See the list: npx uix-ui list`), { missing: true });
95
+ const pro = source(cfg, `@uix-pro/${src.item}`);
96
+ return { src: pro, it: await load(pro) };
97
+ }
98
+ }
99
+
68
100
  /** Loads the items and everything they depend on, dependencies first. */
69
101
  async function resolveAll(cfg, list) {
70
102
  const seen = new Map();
71
103
  const visit = async (name) => {
72
- const src = source(cfg, name);
104
+ let src = source(cfg, name);
73
105
  const key = `${src.ns}/${src.item}`;
74
106
  if (seen.has(key)) return;
75
107
  seen.set(key, null);
76
- const it = await load(src);
77
- for (const d of it.registryDependencies ?? []) await visit(d.startsWith('@') ? d : `${src.ns}/${d}`);
78
- seen.delete(key); seen.set(key, it);
108
+ const bare = !name.startsWith('@');
109
+ const got = bare ? await loadAny(cfg, src) : { src, it: await load(src) };
110
+ const it = got.it;
111
+ if (got.src !== src) { seen.delete(key); src = got.src; seen.set(`${src.ns}/${src.item}`, null); }
112
+ for (const d of it.registryDependencies ?? []) await visit(d.startsWith('@') ? d : `${src.ns}/${/^https?:/.test(d) ? d.split('/').pop().replace(/\.json$/, '') : d}`);
113
+ seen.delete(`${src.ns}/${src.item}`); seen.set(`${src.ns}/${src.item}`, it);
79
114
  };
80
115
  for (const n of list) await visit(n);
81
116
  return [...seen.values()];
@@ -102,6 +137,20 @@ function place(cfg, items, overwrite) {
102
137
  write(file, code);
103
138
  say(` wrote ${shown}`);
104
139
  }
140
+ for (const it of items) for (const f of it.files) if (/\.css$/.test(f.path) && !/(^|\/)uix\.css$/.test(f.path)) importCss(cfg, target(cfg, f));
141
+ }
142
+
143
+ /** Adds `@import "<file>";` to the app's main CSS, after the theme import (visual styles, backdrops). */
144
+ function importCss(cfg, file) {
145
+ const cssFile = join(cwd, cfg.tailwind?.css ?? 'src/index.css');
146
+ if (!existsSync(cssFile)) return;
147
+ let src = readFileSync(cssFile, 'utf8');
148
+ const imp = `@import "${rel(cssFile, file)}";`;
149
+ if (src.includes(imp)) return;
150
+ const last = [...src.matchAll(/@import [^;\n]+;?/g)].pop(); // after the theme and any earlier style sheets
151
+ src = last ? src.slice(0, last.index + last[0].length) + `\n${imp}` + src.slice(last.index + last[0].length) : `${imp}\n${src}`;
152
+ write(cssFile, src);
153
+ say(` updated ${relative(cwd, cssFile)}`);
105
154
  }
106
155
 
107
156
  function install(items) {
@@ -156,7 +205,8 @@ async function init() {
156
205
  cfg.registries = {
157
206
  ...cfg.registries,
158
207
  '@uix': `${registry}/{name}.json`,
159
- '@uix-pro': { url: `${registry.replace(/\/r$/, '/r/pro')}/{name}.json`, headers: { Authorization: 'Bearer ${UIX_LICENSE}' } },
208
+ // a saved system's registry (/r/<id>) already has the owner's Pro items
209
+ ...(/\/r$/.test(registry) && { '@uix-pro': { url: `${registry.replace(/\/r$/, '/r/pro')}/{name}.json`, headers: { Authorization: 'Bearer ${UIX_LICENSE}' } } }),
160
210
  };
161
211
  write(join(cwd, 'components.json'), JSON.stringify(cfg, null, 2) + '\n');
162
212
  say(' wrote components.json');
@@ -173,12 +223,42 @@ async function init() {
173
223
  const items = await resolveAll(cfg, ['utils']);
174
224
  place(cfg, items, false);
175
225
  install(items);
176
- say(`\nDone. Next: npx uix-ui add button card input\nDark mode: <html data-theme="dark">. Other presets: npx uix-ui theme <preset>.`);
226
+ return cfg;
227
+ }
228
+
229
+ /** Your saved system, through the account API (the key proves it's yours): { id, name, version, … }. */
230
+ async function system(site, id) {
231
+ const key = opts.key ?? process.env.UIX_API_KEY;
232
+ if (!key) die(`set UIX_API_KEY (or --key) to the API key from ${site}/account.`);
233
+ const res = await fetch(`${site}/api/me/systems/${id}`, { headers: { Authorization: `Bearer ${key}` } }).catch(e => die(`could not reach ${site} (${e.cause?.code ?? e.message}).`));
234
+ if (res.status === 401) die(`that API key wasn't accepted. Make a new one at ${site}/account.`);
235
+ if (res.status === 404) die(`there's no design system "${id}" in your account (${site}/account#systems).`);
236
+ if (!res.ok) die(`could not load "${id}" (${res.status} from ${site}).`);
237
+ return (await res.json()).system;
238
+ }
239
+
240
+ /** pull: the system's theme, its DTCG tokens and any named components; sync: the same, only when the version moved. */
241
+ async function pull(id, names) {
242
+ const saved = existsSync(join(cwd, 'uix.json')) ? readJson('uix.json') : {};
243
+ id ??= saved.system;
244
+ if (!id) die(cmd === 'sync' ? 'nothing to sync yet. Run `npx uix-ui pull <id>` first (the id is in your Studio link, /s/<id>).' : 'name the design system, e.g. `npx uix-ui pull k3x9d0a2bq` (the id is in your Studio link, /s/<id>).');
245
+ const site = (opts.site ?? saved.site ?? SITE).replace(/\/$/, '');
246
+ const sys = await system(site, id);
247
+ if (cmd === 'sync' && sys.version === saved.version && id === saved.system) return say(`Up to date: "${sys.name}", version ${sys.version}.`);
248
+ pin = `?v=${sys.version}`;
249
+ opts.registry = `${site}/r/${id}`;
250
+ const cfg = await init();
251
+ const tokens = await load({ item: 'tokens', url: `${site}/s/${id}.json`, headers: {} });
252
+ for (const [name, t] of Object.entries(tokens)) { write(join(srcRoot, 'styles', name), JSON.stringify(t, null, 2) + '\n'); say(` wrote ${relative(cwd, join(srcRoot, 'styles', name))}`); }
253
+ if (names.length) { const items = await resolveAll(cfg, names); place(cfg, items, !!opts.overwrite); install(items); }
254
+ write(join(cwd, 'uix.json'), JSON.stringify({ system: id, name: sys.name, version: sys.version, site: site === SITE ? undefined : site }, null, 2) + '\n');
255
+ say(`\n"${sys.name}", version ${sys.version}${saved.version && saved.system === id ? ` (was ${saved.version})` : ''}. Later changes: npx uix-ui sync`);
177
256
  }
178
257
 
179
258
  async function main() {
180
259
  if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') return say(HELP);
181
- if (cmd === 'init') return init();
260
+ if (cmd === 'init') return init().then(() => say(`\nDone. Next: npx uix-ui add button card input\nDark mode: <html data-theme="dark">. Other presets: npx uix-ui theme <preset>.`));
261
+ if (cmd === 'pull' || cmd === 'sync') return pull(cmd === 'pull' ? names[0] : undefined, names.slice(1));
182
262
  const cfg = config();
183
263
  if (cmd === 'add') {
184
264
  if (!names.length) die('name at least one component, e.g. `npx uix-ui add button`.');
@@ -189,10 +269,15 @@ async function main() {
189
269
  if (cmd === 'theme') return applyTheme(cfg, names[0] ?? opts.preset);
190
270
  if (cmd === 'list') {
191
271
  const src = source(cfg, 'x');
192
- const index = await load({ ...src, item: 'registry.json', url: src.url.replace(/\/r\/x\.json$/, '/registry.json') });
272
+ const index = await load({ ...src, item: 'registry.json', url: src.url.replace(/\/r\/x\.json$/, '/registry.json').replace(/\/x\.json$/, '/registry.json') });
193
273
  const comps = index.items.filter(i => i.type === 'registry:ui' && i.name !== 'icons').map(i => i.name);
194
274
  const themes = index.items.filter(i => i.name.startsWith('theme')).map(i => i.name === 'theme' ? 'uix' : i.name.slice(6));
195
- return say(`Components (${comps.length}): ${comps.join(', ')}\n\nThemes (${themes.length}): ${themes.join(', ')}`);
275
+ say(`Components (${comps.length}): ${comps.join(', ')}\n\nThemes (${themes.length}): ${themes.join(', ')}`);
276
+ const pro = await loadProIndex(cfg);
277
+ if (!pro) return;
278
+ const blocks = pro.items.filter(i => !i.name.startsWith('theme-')).map(i => i.name);
279
+ const proThemes = pro.items.filter(i => i.name.startsWith('theme-')).map(i => i.name.slice(6));
280
+ return say(`\nUIX Pro, with UIX_LICENSE (${PRICING}):\n Blocks and styles (${blocks.length}): ${blocks.join(', ')}\n Themes (${proThemes.length}): ${proThemes.join(', ')}`);
196
281
  }
197
282
  die(`unknown command "${cmd}".\n\n${HELP}`);
198
283
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uix-ui",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Add UIX components and themes to your React + Tailwind app: npx uix-ui init.",
5
5
  "license": "MIT",
6
6
  "type": "module",