rippleui-cli 0.1.1 → 0.1.3

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 CHANGED
@@ -1,5 +1,6 @@
1
1
  # ripple-ui
2
- A shadcn/ui inspired component library for Ripple TS.
2
+
3
+ A [shadcn/ui](https://github.com/shadcn-ui/ui) inspired component library for [Ripple TS](https://github.com/Ripple-TS/ripple).
3
4
 
4
5
  ## Getting started
5
6
 
@@ -41,17 +42,20 @@ npx rippleui-cli installed
41
42
  - button
42
43
  - checkbox
43
44
  - input
45
+ - select
44
46
  - label
45
47
  - utils
46
48
 
47
49
  ## Theming
48
50
 
49
51
  ### Base colors:
52
+
50
53
  - stone
51
54
  - zinc
52
55
  - slate
53
56
 
54
57
  ### Accent themes:
58
+
55
59
  - neutral
56
60
  - blue
57
61
  - violet
@@ -61,6 +65,7 @@ npx rippleui-cli installed
61
65
  ## components.json
62
66
 
63
67
  Generated during initialization:
68
+
64
69
  ```json
65
70
  {
66
71
  "aliases": {
package/dist/cli.js CHANGED
@@ -4,6 +4,17 @@ import { add } from "./commands/add.js";
4
4
  import { init } from "./commands/init.js";
5
5
  import { installed } from "./commands/installed.js";
6
6
  import { list } from "./commands/list.js";
7
+ process.on("unhandledRejection", (reason) => {
8
+ const msg = reason instanceof Error ? reason.message : String(reason ?? "Unkown error");
9
+ console.error(`✖ Unexpected error: ${msg}`);
10
+ console.error(" If this keeps happening, please open an issue at https://github.com/radeqq007/ripple-ui/issues");
11
+ process.exit(1);
12
+ });
13
+ process.on("uncaughtException", (err) => {
14
+ console.error(`✖ Unexpected error: ${err.message}`);
15
+ console.error(" If this keeps happening, please open an issue at https://github.com/radeqq007/ripple-ui/issues");
16
+ process.exit(1);
17
+ });
7
18
  const program = new Command();
8
19
  program
9
20
  .command("add <component>")
@@ -1,4 +1,5 @@
1
1
  import { readConfig, requireConfig, writeConfig } from "../lib/config.js";
2
+ import { die } from "../lib/errors.js";
2
3
  import { installEntry } from "../lib/install.js";
3
4
  import { fetchRegistry } from "../lib/registry.js";
4
5
  export const add = async (component) => {
@@ -9,9 +10,8 @@ export const add = async (component) => {
9
10
  }
10
11
  requireConfig(config);
11
12
  const registry = await fetchRegistry();
12
- if (!registry[component]) {
13
- console.error(`Component "${component}" not found.`);
14
- process.exit(1);
13
+ if (!registry[component] || component.startsWith("$")) {
14
+ die(`Component "${component}" not found in the registry.`, `use 'npx rippleui-cli list' to see aviable components.`);
15
15
  }
16
16
  const alreadyInstalled = new Set(config.installed);
17
17
  // component + its dependencies
@@ -28,7 +28,7 @@ export const add = async (component) => {
28
28
  }
29
29
  collectDeps(component);
30
30
  if (toInstall.size === 0) {
31
- console.log(`"${component}" is already installed.`);
31
+ console.log(`✔ "${component}" is already installed.`);
32
32
  return;
33
33
  }
34
34
  console.log(`Installing: ${[...toInstall].join(", ")}\n`);
@@ -1,63 +1,66 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import prompts from 'prompts';
4
- import { updateCss } from '../lib/css.js';
5
- import { detectCssFile, detectImportAlias, detectTailwind, } from '../lib/detect.js';
6
- import { installNpmDeps } from '../lib/install.js';
7
- import { accentThemes, bases } from '../lib/themes.js';
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import prompts from "prompts";
4
+ import { updateCss } from "../lib/css.js";
5
+ import { detectCssFile, detectImportAlias, detectTailwind, } from "../lib/detect.js";
6
+ import { die, formatError } from "../lib/errors.js";
7
+ import { installNpmDeps } from "../lib/install.js";
8
+ import { accentThemes, bases } from "../lib/themes.js";
8
9
  export const init = async () => {
9
10
  const cwd = process.cwd();
10
11
  try {
11
12
  await fs.access(`${cwd}/components.json`);
12
- console.log('✔ components.json already exists. Skipping initialization.');
13
+ console.log("✔ components.json already exists. Skipping initialization.");
13
14
  process.exit(1);
14
15
  }
15
16
  catch { }
16
17
  if (await detectTailwind(cwd)) {
17
- console.log('✔ Validating tailwindcss.');
18
+ console.log("✔ Validating tailwindcss.");
18
19
  }
19
20
  else {
20
- console.error('✖ Tailwind CSS not detected. Please install it first: https://tailwindcss.com/docs/installation');
21
- process.exit(1);
21
+ die("Tailwind CSS not found in your project dependencies.", "Install it first: https://tailwindcss.com/docs/installation");
22
22
  }
23
23
  let mainCssFile = await detectCssFile(cwd);
24
- if (mainCssFile === null) {
25
- console.log('Could not find the main CSS file.');
24
+ if (!mainCssFile) {
25
+ console.log("Could not find the main CSS file.");
26
26
  const response = await prompts([
27
27
  {
28
- type: 'text',
29
- name: 'mainCssFile',
30
- message: 'Where is your main CSS file?',
31
- initial: 'src/index.css',
28
+ type: "text",
29
+ name: "mainCssFile",
30
+ message: "Where is your main CSS file?",
31
+ initial: "src/index.css",
32
32
  },
33
33
  ]);
34
34
  mainCssFile = response.mainCssFile;
35
35
  }
36
+ if (!mainCssFile) {
37
+ die("A valid CSS file path is required to continue.", "Re-run init and provide the path to your main CSS file.");
38
+ }
36
39
  const detectedAlias = await detectImportAlias(cwd);
37
40
  console.log(`✔ Validating import alias. Found "${detectedAlias}".`);
38
- const baseChoices = Object.keys(bases).map(name => ({
41
+ const baseChoices = Object.keys(bases).map((name) => ({
39
42
  title: name.charAt(0).toUpperCase() + name.slice(1),
40
43
  value: name,
41
44
  }));
42
- const accentChoices = Object.keys(accentThemes).map(name => ({
45
+ const accentChoices = Object.keys(accentThemes).map((name) => ({
43
46
  title: name.charAt(0).toUpperCase() + name.slice(1),
44
47
  value: name,
45
48
  }));
46
49
  const { base, accent } = await prompts([
47
50
  {
48
- type: 'select',
49
- name: 'base',
50
- message: 'Select a base color (neutral scale for backgrounds, borders, muted tones):',
51
+ type: "select",
52
+ name: "base",
53
+ message: "Select a base color (neutral scale for backgrounds, borders, muted tones):",
51
54
  choices: baseChoices,
52
55
  },
53
56
  {
54
- type: 'select',
55
- name: 'accent',
56
- message: 'Select an accent theme (primary/brand color and border radius):',
57
+ type: "select",
58
+ name: "accent",
59
+ message: "Select an accent theme (primary/brand color and border radius):",
57
60
  choices: accentChoices,
58
61
  },
59
62
  ]);
60
- console.log('✔ Writing components.json.');
63
+ console.log("✔ Writing components.json.");
61
64
  const config = {
62
65
  aliases: {
63
66
  components: `${detectedAlias}/components`,
@@ -66,13 +69,18 @@ export const init = async () => {
66
69
  css: mainCssFile,
67
70
  installed: [],
68
71
  // TODO: maybe detect those instead of hardcoding the directories
69
- componentsDir: 'src/components',
70
- utilsDir: 'src/utils',
72
+ componentsDir: "src/components",
73
+ utilsDir: "src/utils",
71
74
  };
72
- await fs.writeFile('components.json', JSON.stringify(config, null, 2) + '\n');
75
+ try {
76
+ await fs.writeFile("components.json", `${JSON.stringify(config, null, 2)}\n`);
77
+ }
78
+ catch (e) {
79
+ die(`Could not write components.json: ${formatError(e)}`, "Make sure you have write permission in the current directory.");
80
+ }
73
81
  await updateCss(path.join(cwd, mainCssFile), base, accent);
74
82
  console.log(`✔ Updating ${mainCssFile}`);
75
- console.log('\tInstalling dependencies...');
76
- await installNpmDeps(['@fontsource-variable/geist', 'tw-animate-css'], cwd);
77
- console.log('Done.');
83
+ console.log("\tInstalling dependencies...");
84
+ await installNpmDeps(["@fontsource-variable/geist", "tw-animate-css"], cwd);
85
+ console.log("Done.");
78
86
  };
@@ -1,9 +1,9 @@
1
1
  import { readConfig } from "../lib/config.js";
2
+ import { die } from "../lib/errors.js";
2
3
  export const installed = async () => {
3
4
  const config = await readConfig();
4
5
  if (!config) {
5
- console.error("Could not read the components.json file.");
6
- process.exit(1);
6
+ die("No components.json found.", 'Run "npx rippleui-cli init" first to set up your project.');
7
7
  }
8
8
  if (config.installed.length === 0) {
9
9
  console.log("No components installed yet.");
@@ -1,7 +1,7 @@
1
1
  import { fetchRegistry } from "../lib/registry.js";
2
2
  export const list = async () => {
3
3
  const registry = await fetchRegistry();
4
- const names = Object.keys(registry);
4
+ const names = Object.keys(registry).filter((k) => !k.startsWith("$"));
5
5
  console.log("Available components:");
6
6
  for (const name of names) {
7
7
  console.log(` ${name}`);
@@ -0,0 +1,28 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { die, formatError } from "./errors.js";
4
+ const configPath = path.resolve(process.cwd(), "components.json");
5
+ export const readConfig = async () => {
6
+ let raw;
7
+ try {
8
+ raw = await fs.readFile(configPath, "utf-8");
9
+ }
10
+ catch (e) {
11
+ die(`Could not read components.json: ${formatError(e)}`);
12
+ }
13
+ try {
14
+ return JSON.parse(raw);
15
+ }
16
+ catch {
17
+ die("components.json contains invalid JSON.", `Fix or delete ${configPath} and run "rippleui-cli init" again.`);
18
+ }
19
+ };
20
+ export const writeConfig = async (config) => {
21
+ await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
22
+ };
23
+ export const requireConfig = (config) => {
24
+ if (!config) {
25
+ console.error('No components.json found. Run "ripple-ui init" first.');
26
+ process.exit(1);
27
+ }
28
+ };
@@ -0,0 +1,168 @@
1
+ import fs from "node:fs/promises";
2
+ import { die } from "./errors.js";
3
+ import { accentThemes, bases } from "./themes.js";
4
+ export const updateCss = async (cssPath, baseName, accentName) => {
5
+ const marker = "/* Ripple UI Theme */";
6
+ let existingContent = "";
7
+ try {
8
+ existingContent = await fs.readFile(cssPath, "utf-8");
9
+ }
10
+ catch {
11
+ die(`Could not read CSS file: ${cssPath}`, `Check that the file exists and is readable.`);
12
+ }
13
+ const markerIdx = existingContent.indexOf(marker);
14
+ const userContent = markerIdx !== -1
15
+ ? existingContent.slice(0, markerIdx).trimEnd()
16
+ : existingContent.trimEnd();
17
+ const b = bases[baseName];
18
+ if (!b)
19
+ die(`Unknown base color: ${baseName}`);
20
+ const a = accentThemes[accentName];
21
+ if (!a)
22
+ die(`Unkown accent theme: ${accentName}`);
23
+ // Typescript doesn't detect the never type in die()
24
+ // Those checks are always false
25
+ // They are here just to make typescript shut up
26
+ if (!a)
27
+ return;
28
+ if (!b)
29
+ return;
30
+ const content = `/* Ripple UI Theme */
31
+ @import "tw-animate-css";
32
+ @import "@fontsource-variable/geist";
33
+
34
+ @custom-variant dark (&:is(.dark *));
35
+
36
+ @theme inline {
37
+ --font-heading: var(--font-sans);
38
+ --font-sans: 'Geist Variable', sans-serif;
39
+ --color-sidebar-ring: var(--sidebar-ring);
40
+ --color-sidebar-border: var(--sidebar-border);
41
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
42
+ --color-sidebar-accent: var(--sidebar-accent);
43
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
44
+ --color-sidebar-primary: var(--sidebar-primary);
45
+ --color-sidebar-foreground: var(--sidebar-foreground);
46
+ --color-sidebar: var(--sidebar);
47
+ --color-chart-5: var(--chart-5);
48
+ --color-chart-4: var(--chart-4);
49
+ --color-chart-3: var(--chart-3);
50
+ --color-chart-2: var(--chart-2);
51
+ --color-chart-1: var(--chart-1);
52
+ --color-ring: var(--ring);
53
+ --color-input: var(--input);
54
+ --color-border: var(--border);
55
+ --color-destructive: var(--destructive);
56
+ --color-accent-foreground: var(--accent-foreground);
57
+ --color-accent: var(--accent);
58
+ --color-muted-foreground: var(--muted-foreground);
59
+ --color-muted: var(--muted);
60
+ --color-secondary-foreground: var(--secondary-foreground);
61
+ --color-secondary: var(--secondary);
62
+ --color-primary-foreground: var(--primary-foreground);
63
+ --color-primary: var(--primary);
64
+ --color-popover-foreground: var(--popover-foreground);
65
+ --color-popover: var(--popover);
66
+ --color-card-foreground: var(--card-foreground);
67
+ --color-card: var(--card);
68
+ --color-foreground: var(--foreground);
69
+ --color-background: var(--background);
70
+ --radius-sm: calc(var(--radius) * 0.6);
71
+ --radius-md: calc(var(--radius) * 0.8);
72
+ --radius-lg: var(--radius);
73
+ --radius-xl: calc(var(--radius) * 1.4);
74
+ --radius-2xl: calc(var(--radius) * 1.8);
75
+ --radius-3xl: calc(var(--radius) * 2.2);
76
+ --radius-4xl: calc(var(--radius) * 2.6);
77
+ }
78
+
79
+ :root {
80
+ --background: ${b.root.bg};
81
+ --foreground: ${b.root.fg};
82
+ --card: ${b.root.card.bg};
83
+ --card-foreground: ${b.root.card.fg};
84
+ --popover: ${b.root.popover.bg};
85
+ --popover-foreground: ${b.root.popover.fg};
86
+ --primary: ${a.root.primary.bg};
87
+ --primary-foreground: ${a.root.primary.fg};
88
+ --secondary: ${b.root.secondary.bg};
89
+ --secondary-foreground: ${b.root.secondary.fg};
90
+ --muted: ${b.root.muted.bg};
91
+ --muted-foreground: ${b.root.muted.fg};
92
+ --accent: ${b.root.accent.bg};
93
+ --accent-foreground: ${b.root.accent.fg};
94
+ --destructive: ${b.root.destructive};
95
+ --border: ${b.root.border};
96
+ --input: ${b.root.input};
97
+ --ring: ${b.root.ring};
98
+ --chart-1: ${b.root.charts[0]};
99
+ --chart-2: ${b.root.charts[1]};
100
+ --chart-3: ${b.root.charts[2]};
101
+ --chart-4: ${b.root.charts[3]};
102
+ --chart-5: ${b.root.charts[4]};
103
+ --radius: ${a.radius};
104
+ --sidebar: ${b.root.sidebar.bg};
105
+ --sidebar-foreground: ${b.root.sidebar.fg};
106
+ --sidebar-primary: ${a.root.sidebar.primary.bg};
107
+ --sidebar-primary-foreground: ${a.root.sidebar.primary.fg};
108
+ --sidebar-accent: ${b.root.sidebar.accent.bg};
109
+ --sidebar-accent-foreground: ${b.root.sidebar.accent.fg};
110
+ --sidebar-border: ${b.root.sidebar.border};
111
+ --sidebar-ring: ${b.root.sidebar.ring};
112
+ }
113
+
114
+ .dark {
115
+ --background: ${b.dark.bg};
116
+ --foreground: ${b.dark.fg};
117
+ --card: ${b.dark.card.bg};
118
+ --card-foreground: ${b.dark.card.fg};
119
+ --popover: ${b.dark.popover.bg};
120
+ --popover-foreground: ${b.dark.popover.fg};
121
+ --primary: ${a.dark.primary.bg};
122
+ --primary-foreground: ${a.dark.primary.fg};
123
+ --secondary: ${b.dark.secondary.bg};
124
+ --secondary-foreground: ${b.dark.secondary.fg};
125
+ --muted: ${b.dark.muted.bg};
126
+ --muted-foreground: ${b.dark.muted.fg};
127
+ --accent: ${b.dark.accent.bg};
128
+ --accent-foreground: ${b.dark.accent.fg};
129
+ --destructive: ${b.dark.destructive};
130
+ --border: ${b.dark.border};
131
+ --input: ${b.dark.input};
132
+ --ring: ${b.dark.ring};
133
+ --chart-1: ${b.dark.charts[0]};
134
+ --chart-2: ${b.dark.charts[1]};
135
+ --chart-3: ${b.dark.charts[2]};
136
+ --chart-4: ${b.dark.charts[3]};
137
+ --chart-5: ${b.dark.charts[4]};
138
+ --sidebar: ${b.dark.sidebar.bg};
139
+ --sidebar-foreground: ${b.dark.sidebar.fg};
140
+ --sidebar-primary: ${a.dark.sidebar.primary.bg};
141
+ --sidebar-primary-foreground: ${a.dark.sidebar.primary.fg};
142
+ --sidebar-accent: ${b.dark.sidebar.accent.bg};
143
+ --sidebar-accent-foreground: ${b.dark.sidebar.accent.fg};
144
+ --sidebar-border: ${b.dark.sidebar.border};
145
+ --sidebar-ring: ${b.dark.sidebar.ring};
146
+ }
147
+
148
+ @layer base {
149
+ * {
150
+ @apply border-border outline-ring/50;
151
+ }
152
+ body {
153
+ @apply bg-background text-foreground;
154
+ }
155
+ html {
156
+ @apply font-sans;
157
+ }
158
+ }`;
159
+ const newContent = userContent
160
+ ? `${userContent}\n\n${content}\n`
161
+ : `${content}\n`;
162
+ try {
163
+ await fs.writeFile(cssPath, newContent);
164
+ }
165
+ catch (e) {
166
+ die(`Could not write into ${cssPath}: ${e.message}`);
167
+ }
168
+ };
@@ -0,0 +1,95 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { die, formatError } from "./errors.js";
4
+ export const detectPackageManager = async (cwd) => {
5
+ try {
6
+ const raw = await fs.readFile(path.join(cwd, "package.json"), "utf-8");
7
+ const pkg = JSON.parse(raw);
8
+ if (pkg.packageManager) {
9
+ // e.g. "pnpm@10.32.1" → "pnpm"
10
+ return pkg.packageManager.split("@")[0];
11
+ }
12
+ }
13
+ catch { }
14
+ return "npm";
15
+ };
16
+ export const detectTailwind = async (cwd) => {
17
+ let raw;
18
+ try {
19
+ raw = await fs.readFile(path.join(cwd, "package.json"), "utf-8");
20
+ }
21
+ catch (e) {
22
+ die(`Could not read package.json: ${formatError(e)}`);
23
+ }
24
+ try {
25
+ const pkg = JSON.parse(raw);
26
+ const deps = {
27
+ ...pkg.dependencies,
28
+ ...pkg.devDependencies,
29
+ };
30
+ return "tailwindcss" in deps;
31
+ }
32
+ catch {
33
+ die("package.json contains invalid JSON.", "Fix your package.json and try again.");
34
+ return false;
35
+ }
36
+ };
37
+ export const detectCssFile = async (cwd) => {
38
+ const paths = ["src/index.css", "src/main.css", "src/styles.css"];
39
+ for (const relPath of paths) {
40
+ const fullPath = path.resolve(cwd, relPath);
41
+ try {
42
+ await fs.access(fullPath);
43
+ console.log(`✔ Found main CSS file: ${relPath}`);
44
+ return relPath;
45
+ }
46
+ catch { }
47
+ }
48
+ return null;
49
+ };
50
+ export const detectImportAlias = async (cwd) => {
51
+ const viteConfigNames = [
52
+ "vite.config.ts",
53
+ "vite.config.js",
54
+ "vite.config.mts",
55
+ "vite.config.mjs",
56
+ ];
57
+ for (const configName of viteConfigNames) {
58
+ const fullPath = path.join(cwd, configName);
59
+ try {
60
+ const raw = await fs.readFile(fullPath, "utf-8");
61
+ const objectAliasMatch = raw.match(/alias\s*:\s*\{([^}]+)\}/s);
62
+ if (objectAliasMatch?.[1]) {
63
+ const entryMatch = objectAliasMatch[1].match(/['"]?(@[^'":/\s]+|[^'":/\s]+)['"]?\s*:/);
64
+ if (entryMatch?.[1]) {
65
+ return entryMatch[1];
66
+ }
67
+ }
68
+ const arrayFindMatch = raw.match(/find\s*:\s*['"]([^'"]+)['"]/);
69
+ if (arrayFindMatch?.[1]) {
70
+ return arrayFindMatch[1];
71
+ }
72
+ }
73
+ catch { }
74
+ }
75
+ // Fall back to tsconfig.json
76
+ try {
77
+ const raw = await fs.readFile(path.join(cwd, "tsconfig.json"), "utf-8");
78
+ let tsconfig;
79
+ try {
80
+ tsconfig = JSON.parse(raw);
81
+ }
82
+ catch {
83
+ die("tsconfig.json contains invalid JSON.", "Fix your tsconfig.json and try again.");
84
+ }
85
+ const paths = tsconfig.compilerOptions?.paths ?? {};
86
+ for (const alias of Object.keys(paths)) {
87
+ return alias.replace(/\/\*$/, "");
88
+ }
89
+ }
90
+ catch (e) {
91
+ die(`Could not read tsconfig.json: ${formatError(e)}`);
92
+ }
93
+ console.error("No import alias found.");
94
+ process.exit(1);
95
+ };
@@ -0,0 +1,26 @@
1
+ export const die = (msg, hint) => {
2
+ console.error(`✖ ${msg}`);
3
+ if (hint)
4
+ console.error(` ${hint}`);
5
+ return process.exit(1);
6
+ };
7
+ export const warn = (msg) => {
8
+ console.warn(`⚠ ${msg}`);
9
+ };
10
+ export const isNetworkError = (err) => {
11
+ if (!(err instanceof Error))
12
+ return false;
13
+ return (err.message.includes("fetch") ||
14
+ err.message.includes("ENOTFOUND") ||
15
+ err.message.includes("ECONNREFUSED") ||
16
+ err.message.includes("ETIMEDOUT") ||
17
+ err.message.includes("network") ||
18
+ err.message.toLowerCase().includes("failed to fetch"));
19
+ };
20
+ export const formatError = (err) => {
21
+ if (err instanceof Error)
22
+ return err.message;
23
+ if (typeof err === "string")
24
+ return err;
25
+ return "An unknown error occurred";
26
+ };
@@ -0,0 +1,70 @@
1
+ import { execSync } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { detectPackageManager } from "./detect.js";
5
+ import { die } from "./errors.js";
6
+ import { fetchFile, fetchRegistry } from "./registry.js";
7
+ export const resolveTargetDir = (name, entry, config) => {
8
+ // utils
9
+ if (entry.target)
10
+ return config.utilsDir ?? entry.target;
11
+ return `${config.componentsDir}/${name}`;
12
+ };
13
+ export async function installNpmDeps(packages, cwd) {
14
+ if (packages.length === 0)
15
+ return;
16
+ const pm = await detectPackageManager(cwd);
17
+ const cmd = pm === "npm" ? "npm install" : `${pm} add`;
18
+ console.log(`\nInstalling npm packages with ${pm}: ${packages.join(", ")}`);
19
+ try {
20
+ execSync(`${cmd} ${packages.join(" ")}`, { cwd, stdio: "inherit" });
21
+ }
22
+ catch {
23
+ die(`Failed to install npm packages: ${packages.join(", ")}`, `Try running "${cmd} ${packages.join(" ")}" manually, then re-run this command.`);
24
+ }
25
+ }
26
+ export const installEntry = async (name, config, installed = new Set(), npmDeps = new Set()) => {
27
+ if (installed.has(name))
28
+ return;
29
+ installed.add(name);
30
+ const registry = await fetchRegistry();
31
+ const entry = registry[name];
32
+ if (!entry)
33
+ die(`"${name}" not found in registry`, 'Run "rippleui-cli list" to see all available components.');
34
+ for (const pkg of entry.npmDependencies ?? []) {
35
+ npmDeps.add(pkg);
36
+ }
37
+ for (const dep of entry.dependencies ?? []) {
38
+ await installEntry(dep, config, installed, npmDeps);
39
+ }
40
+ const targetDir = resolveTargetDir(name, entry, config);
41
+ const targetPath = path.resolve(process.cwd(), targetDir);
42
+ try {
43
+ await fs.mkdir(targetPath, { recursive: true });
44
+ }
45
+ catch {
46
+ die(`Could not create directory: ${targetPath}`, `Check that you have write permission here: ${path.dirname(targetPath)}`);
47
+ }
48
+ await Promise.all(entry.files.map(async (file) => {
49
+ let content = await fetchFile(`${entry.path}/${file}`);
50
+ content = updateImports(content, config);
51
+ const dest = path.resolve(targetPath, file);
52
+ try {
53
+ await fs.writeFile(dest, content);
54
+ }
55
+ catch {
56
+ die(`Could not write file: ${dest}`, `Check that you have write permission in: ${targetPath}`);
57
+ }
58
+ }));
59
+ console.log(`✔ Installed: ${name} → ${targetPath}`);
60
+ if (npmDeps.size > 0) {
61
+ await installNpmDeps([...npmDeps], process.cwd());
62
+ npmDeps.clear();
63
+ }
64
+ };
65
+ export const updateImports = (content, config) => {
66
+ return content
67
+ .replace(/@\/components/g, config.aliases.components)
68
+ .replace(/@\/lib/g, config.aliases.utils)
69
+ .replace(/@\/utils/g, config.aliases.utils);
70
+ };
@@ -0,0 +1,59 @@
1
+ import { die, formatError, isNetworkError } from "./errors.js";
2
+ const GITHUB_RAW = "https://raw.githubusercontent.com/radeqq007/ripple-ui/main";
3
+ const REGISTRY_URL = `${GITHUB_RAW}/registry.json`;
4
+ export const fetchRegistry = async () => {
5
+ let res;
6
+ try {
7
+ res = await fetch(REGISTRY_URL);
8
+ }
9
+ catch (e) {
10
+ if (isNetworkError(e)) {
11
+ die("Could not reach the ripple-ui registry.", "Check your internet connection and try again.");
12
+ }
13
+ die(`Failed to fetch registry: ${formatError(e)}`);
14
+ }
15
+ if (!res.ok) {
16
+ if (res.status === 404)
17
+ die("Registry not found (404).", "The registry URL may have changed. Please update rippleui-cli.");
18
+ if (res.status >= 500)
19
+ die(`Registry server error (${res.status}).`, "GitHub may be experiencing issues. Try again in a moment.");
20
+ die(`Failed to fetch registry: HTTP ${res.status} ${res.statusText}`);
21
+ }
22
+ try {
23
+ return res.json();
24
+ }
25
+ catch {
26
+ die("Registry response was not valid JSON.", "This may be a temporary issue - please try again.");
27
+ process.exit(1);
28
+ }
29
+ };
30
+ export const fetchFile = async (filePath) => {
31
+ const url = `${GITHUB_RAW}/${filePath}`;
32
+ let res;
33
+ try {
34
+ res = await fetch(url);
35
+ }
36
+ catch (e) {
37
+ if (isNetworkError(e)) {
38
+ die("Lost connection while downloading component files.", "Check your internet connection and try again.");
39
+ }
40
+ die(`Failed to fetch file "${filePath}": ${formatError(e)}`);
41
+ }
42
+ // Typescript doesn't detect the never type from die()
43
+ // This check is always false, it's there to make typescript shut up
44
+ if (!res)
45
+ process.exit(1);
46
+ if (!res.ok) {
47
+ if (res.status === 404) {
48
+ die(`Component file not found: ${filePath}`, "The registry may be out of sync. Try updating rippleui-cli.");
49
+ }
50
+ die(`Failed to fetch file "${filePath}": HTTP ${res.status} ${res.statusText}`);
51
+ }
52
+ try {
53
+ return await res.text();
54
+ }
55
+ catch (err) {
56
+ die(`Failed to read file content for "${filePath}": ${formatError(err)}`);
57
+ process.exit(1);
58
+ }
59
+ };
@@ -0,0 +1,332 @@
1
+ // ─── Shared types ────────────────────────────────────────────────────────────
2
+ export const bases = {
3
+ stone: {
4
+ root: {
5
+ bg: "oklch(1 0 0)",
6
+ fg: "oklch(0.145 0 0)",
7
+ card: { bg: "oklch(1 0 0)", fg: "oklch(0.145 0 0)" },
8
+ popover: { bg: "oklch(1 0 0)", fg: "oklch(0.145 0 0)" },
9
+ secondary: { bg: "oklch(0.97 0 0)", fg: "oklch(0.205 0 0)" },
10
+ muted: { bg: "oklch(0.97 0 0)", fg: "oklch(0.556 0 0)" },
11
+ accent: { bg: "oklch(0.97 0 0)", fg: "oklch(0.205 0 0)" },
12
+ destructive: "oklch(0.577 0.245 27.325)",
13
+ border: "oklch(0.922 0 0)",
14
+ input: "oklch(0.922 0 0)",
15
+ ring: "oklch(0.708 0 0)",
16
+ charts: [
17
+ "oklch(0.87 0 0)",
18
+ "oklch(0.556 0 0)",
19
+ "oklch(0.439 0 0)",
20
+ "oklch(0.371 0 0)",
21
+ "oklch(0.269 0 0)",
22
+ ],
23
+ sidebar: {
24
+ bg: "oklch(0.985 0 0)",
25
+ fg: "oklch(0.145 0 0)",
26
+ accent: { bg: "oklch(0.97 0 0)", fg: "oklch(0.205 0 0)" },
27
+ border: "oklch(0.922 0 0)",
28
+ ring: "oklch(0.708 0 0)",
29
+ },
30
+ },
31
+ dark: {
32
+ bg: "oklch(0.145 0 0)",
33
+ fg: "oklch(0.985 0 0)",
34
+ card: { bg: "oklch(0.205 0 0)", fg: "oklch(0.985 0 0)" },
35
+ popover: { bg: "oklch(0.205 0 0)", fg: "oklch(0.985 0 0)" },
36
+ secondary: { bg: "oklch(0.269 0 0)", fg: "oklch(0.985 0 0)" },
37
+ muted: { bg: "oklch(0.269 0 0)", fg: "oklch(0.708 0 0)" },
38
+ accent: { bg: "oklch(0.269 0 0)", fg: "oklch(0.985 0 0)" },
39
+ destructive: "oklch(0.704 0.191 22.216)",
40
+ border: "oklch(1 0 0 / 10%)",
41
+ input: "oklch(1 0 0 / 15%)",
42
+ ring: "oklch(0.556 0 0)",
43
+ charts: [
44
+ "oklch(0.87 0 0)",
45
+ "oklch(0.556 0 0)",
46
+ "oklch(0.439 0 0)",
47
+ "oklch(0.371 0 0)",
48
+ "oklch(0.269 0 0)",
49
+ ],
50
+ sidebar: {
51
+ bg: "oklch(0.205 0 0)",
52
+ fg: "oklch(0.985 0 0)",
53
+ accent: { bg: "oklch(0.269 0 0)", fg: "oklch(0.985 0 0)" },
54
+ border: "oklch(1 0 0 / 10%)",
55
+ ring: "oklch(0.556 0 0)",
56
+ },
57
+ },
58
+ },
59
+ zinc: {
60
+ root: {
61
+ bg: "oklch(1 0 0)",
62
+ fg: "oklch(0.141 0.005 285.823)",
63
+ card: { bg: "oklch(1 0 0)", fg: "oklch(0.141 0.005 285.823)" },
64
+ popover: { bg: "oklch(1 0 0)", fg: "oklch(0.141 0.005 285.823)" },
65
+ secondary: {
66
+ bg: "oklch(0.967 0.001 286.375)",
67
+ fg: "oklch(0.21 0.006 285.885)",
68
+ },
69
+ muted: {
70
+ bg: "oklch(0.967 0.001 286.375)",
71
+ fg: "oklch(0.552 0.016 285.938)",
72
+ },
73
+ accent: {
74
+ bg: "oklch(0.967 0.001 286.375)",
75
+ fg: "oklch(0.21 0.006 285.885)",
76
+ },
77
+ destructive: "oklch(0.577 0.245 27.325)",
78
+ border: "oklch(0.92 0.004 286.32)",
79
+ input: "oklch(0.92 0.004 286.32)",
80
+ ring: "oklch(0.705 0.015 286.067)",
81
+ charts: [
82
+ "oklch(0.869 0.005 286.286)",
83
+ "oklch(0.552 0.016 285.938)",
84
+ "oklch(0.442 0.017 285.786)",
85
+ "oklch(0.37 0.013 285.805)",
86
+ "oklch(0.274 0.006 286.033)",
87
+ ],
88
+ sidebar: {
89
+ bg: "oklch(0.985 0.002 286.067)",
90
+ fg: "oklch(0.141 0.005 285.823)",
91
+ accent: {
92
+ bg: "oklch(0.967 0.001 286.375)",
93
+ fg: "oklch(0.21 0.006 285.885)",
94
+ },
95
+ border: "oklch(0.92 0.004 286.32)",
96
+ ring: "oklch(0.705 0.015 286.067)",
97
+ },
98
+ },
99
+ dark: {
100
+ bg: "oklch(0.141 0.005 285.823)",
101
+ fg: "oklch(0.985 0 0)",
102
+ card: {
103
+ bg: "oklch(0.21 0.006 285.885)",
104
+ fg: "oklch(0.985 0 0)",
105
+ },
106
+ popover: {
107
+ bg: "oklch(0.21 0.006 285.885)",
108
+ fg: "oklch(0.985 0 0)",
109
+ },
110
+ secondary: {
111
+ bg: "oklch(0.274 0.006 286.033)",
112
+ fg: "oklch(0.985 0 0)",
113
+ },
114
+ muted: {
115
+ bg: "oklch(0.274 0.006 286.033)",
116
+ fg: "oklch(0.705 0.015 286.067)",
117
+ },
118
+ accent: {
119
+ bg: "oklch(0.274 0.006 286.033)",
120
+ fg: "oklch(0.985 0 0)",
121
+ },
122
+ destructive: "oklch(0.704 0.191 22.216)",
123
+ border: "oklch(1 0 0 / 10%)",
124
+ input: "oklch(1 0 0 / 15%)",
125
+ ring: "oklch(0.552 0.016 285.938)",
126
+ charts: [
127
+ "oklch(0.869 0.005 286.286)",
128
+ "oklch(0.552 0.016 285.938)",
129
+ "oklch(0.442 0.017 285.786)",
130
+ "oklch(0.37 0.013 285.805)",
131
+ "oklch(0.274 0.006 286.033)",
132
+ ],
133
+ sidebar: {
134
+ bg: "oklch(0.21 0.006 285.885)",
135
+ fg: "oklch(0.985 0 0)",
136
+ accent: {
137
+ bg: "oklch(0.274 0.006 286.033)",
138
+ fg: "oklch(0.985 0 0)",
139
+ },
140
+ border: "oklch(1 0 0 / 10%)",
141
+ ring: "oklch(0.552 0.016 285.938)",
142
+ },
143
+ },
144
+ },
145
+ slate: {
146
+ root: {
147
+ bg: "oklch(1 0 0)",
148
+ fg: "oklch(0.129 0.042 264.695)",
149
+ card: { bg: "oklch(1 0 0)", fg: "oklch(0.129 0.042 264.695)" },
150
+ popover: { bg: "oklch(1 0 0)", fg: "oklch(0.129 0.042 264.695)" },
151
+ secondary: {
152
+ bg: "oklch(0.968 0.007 247.896)",
153
+ fg: "oklch(0.208 0.042 265.755)",
154
+ },
155
+ muted: {
156
+ bg: "oklch(0.968 0.007 247.896)",
157
+ fg: "oklch(0.554 0.046 257.417)",
158
+ },
159
+ accent: {
160
+ bg: "oklch(0.968 0.007 247.896)",
161
+ fg: "oklch(0.208 0.042 265.755)",
162
+ },
163
+ destructive: "oklch(0.577 0.245 27.325)",
164
+ border: "oklch(0.929 0.013 255.508)",
165
+ input: "oklch(0.929 0.013 255.508)",
166
+ ring: "oklch(0.704 0.04 256.788)",
167
+ charts: [
168
+ "oklch(0.868 0.017 252.894)",
169
+ "oklch(0.554 0.046 257.417)",
170
+ "oklch(0.446 0.043 257.281)",
171
+ "oklch(0.372 0.044 257.287)",
172
+ "oklch(0.279 0.041 260.031)",
173
+ ],
174
+ sidebar: {
175
+ bg: "oklch(0.984 0.003 247.858)",
176
+ fg: "oklch(0.129 0.042 264.695)",
177
+ accent: {
178
+ bg: "oklch(0.968 0.007 247.896)",
179
+ fg: "oklch(0.208 0.042 265.755)",
180
+ },
181
+ border: "oklch(0.929 0.013 255.508)",
182
+ ring: "oklch(0.704 0.04 256.788)",
183
+ },
184
+ },
185
+ dark: {
186
+ bg: "oklch(0.129 0.042 264.695)",
187
+ fg: "oklch(0.984 0.003 247.858)",
188
+ card: {
189
+ bg: "oklch(0.208 0.042 265.755)",
190
+ fg: "oklch(0.984 0.003 247.858)",
191
+ },
192
+ popover: {
193
+ bg: "oklch(0.208 0.042 265.755)",
194
+ fg: "oklch(0.984 0.003 247.858)",
195
+ },
196
+ secondary: {
197
+ bg: "oklch(0.279 0.041 260.031)",
198
+ fg: "oklch(0.984 0.003 247.858)",
199
+ },
200
+ muted: {
201
+ bg: "oklch(0.279 0.041 260.031)",
202
+ fg: "oklch(0.704 0.04 256.788)",
203
+ },
204
+ accent: {
205
+ bg: "oklch(0.279 0.041 260.031)",
206
+ fg: "oklch(0.984 0.003 247.858)",
207
+ },
208
+ destructive: "oklch(0.704 0.191 22.216)",
209
+ border: "oklch(1 0 0 / 10%)",
210
+ input: "oklch(1 0 0 / 15%)",
211
+ ring: "oklch(0.554 0.046 257.417)",
212
+ charts: [
213
+ "oklch(0.868 0.017 252.894)",
214
+ "oklch(0.554 0.046 257.417)",
215
+ "oklch(0.446 0.043 257.281)",
216
+ "oklch(0.372 0.044 257.287)",
217
+ "oklch(0.279 0.041 260.031)",
218
+ ],
219
+ sidebar: {
220
+ bg: "oklch(0.208 0.042 265.755)",
221
+ fg: "oklch(0.984 0.003 247.858)",
222
+ accent: {
223
+ bg: "oklch(0.279 0.041 260.031)",
224
+ fg: "oklch(0.984 0.003 247.858)",
225
+ },
226
+ border: "oklch(1 0 0 / 10%)",
227
+ ring: "oklch(0.554 0.046 257.417)",
228
+ },
229
+ },
230
+ },
231
+ };
232
+ export const accentThemes = {
233
+ neutral: {
234
+ radius: "0.625rem",
235
+ root: {
236
+ primary: { bg: "oklch(0.205 0 0)", fg: "oklch(0.986 0 0)" },
237
+ sidebar: {
238
+ primary: { bg: "oklch(0.205 0 0)", fg: "oklch(0.985 0 0)" },
239
+ },
240
+ },
241
+ dark: {
242
+ primary: { bg: "oklch(0.922 0 0)", fg: "oklch(0.205 0 0)" },
243
+ sidebar: {
244
+ primary: { bg: "oklch(0.488 0.243 264.376)", fg: "oklch(0.985 0 0)" },
245
+ },
246
+ },
247
+ },
248
+ blue: {
249
+ radius: "0.5rem",
250
+ root: {
251
+ primary: { bg: "oklch(0.546 0.245 262.881)", fg: "oklch(0.985 0 0)" },
252
+ sidebar: {
253
+ primary: {
254
+ bg: "oklch(0.546 0.245 262.881)",
255
+ fg: "oklch(0.985 0 0)",
256
+ },
257
+ },
258
+ },
259
+ dark: {
260
+ primary: { bg: "oklch(0.623 0.214 259.815)", fg: "oklch(0.985 0 0)" },
261
+ sidebar: {
262
+ primary: {
263
+ bg: "oklch(0.623 0.214 259.815)",
264
+ fg: "oklch(0.985 0 0)",
265
+ },
266
+ },
267
+ },
268
+ },
269
+ violet: {
270
+ radius: "0.75rem",
271
+ root: {
272
+ primary: { bg: "oklch(0.541 0.281 293.009)", fg: "oklch(0.985 0 0)" },
273
+ sidebar: {
274
+ primary: {
275
+ bg: "oklch(0.541 0.281 293.009)",
276
+ fg: "oklch(0.985 0 0)",
277
+ },
278
+ },
279
+ },
280
+ dark: {
281
+ primary: { bg: "oklch(0.606 0.25 292.717)", fg: "oklch(0.985 0 0)" },
282
+ sidebar: {
283
+ primary: {
284
+ bg: "oklch(0.606 0.25 292.717)",
285
+ fg: "oklch(0.985 0 0)",
286
+ },
287
+ },
288
+ },
289
+ },
290
+ rose: {
291
+ radius: "0.5rem",
292
+ root: {
293
+ primary: { bg: "oklch(0.586 0.253 17.585)", fg: "oklch(0.985 0 0)" },
294
+ sidebar: {
295
+ primary: {
296
+ bg: "oklch(0.586 0.253 17.585)",
297
+ fg: "oklch(0.985 0 0)",
298
+ },
299
+ },
300
+ },
301
+ dark: {
302
+ primary: { bg: "oklch(0.645 0.246 16.439)", fg: "oklch(0.985 0 0)" },
303
+ sidebar: {
304
+ primary: {
305
+ bg: "oklch(0.645 0.246 16.439)",
306
+ fg: "oklch(0.985 0 0)",
307
+ },
308
+ },
309
+ },
310
+ },
311
+ orange: {
312
+ radius: "0.625rem",
313
+ root: {
314
+ primary: { bg: "oklch(0.646 0.222 41.116)", fg: "oklch(0.985 0 0)" },
315
+ sidebar: {
316
+ primary: {
317
+ bg: "oklch(0.646 0.222 41.116)",
318
+ fg: "oklch(0.985 0 0)",
319
+ },
320
+ },
321
+ },
322
+ dark: {
323
+ primary: { bg: "oklch(0.702 0.191 42.842)", fg: "oklch(0.985 0 0)" },
324
+ sidebar: {
325
+ primary: {
326
+ bg: "oklch(0.702 0.191 42.842)",
327
+ fg: "oklch(0.985 0 0)",
328
+ },
329
+ },
330
+ },
331
+ },
332
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rippleui-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "shadcn/ui inspired components library for Ripple TS",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -8,7 +8,7 @@
8
8
  "watch": "tsc --watch",
9
9
  "lint": "biome check .",
10
10
  "format": "biome format . --write",
11
- "check": "biome check . --all",
11
+ "check": "biome check .",
12
12
  "prepublishOnly": "npm run build"
13
13
  },
14
14
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "license": "MIT",
26
26
  "repository": {
27
27
  "type": "git",
28
- "url": "https://github.com/radeqq007/ripple-ui.git"
28
+ "url": "git+https://github.com/radeqq007/ripple-ui.git"
29
29
  },
30
30
  "homepage": "https://github.com/radeqq007/ripple-ui#readme",
31
31
  "bugs": {
package/registry.json CHANGED
@@ -1,4 +1,5 @@
1
1
  {
2
+ "$schema": "https://raw.githubusercontent.com/radeqq007/ripple-ui/main/registry.schema.json",
2
3
  "button": {
3
4
  "files": ["Button.ripple"],
4
5
  "path": "components/button",
@@ -17,7 +18,13 @@
17
18
  },
18
19
  "checkbox": {
19
20
  "files": ["Checkbox.ripple"],
20
- "path":"components/checkbox",
21
+ "path": "components/checkbox",
22
+ "dependencies": ["utils"],
23
+ "npmDependencies": ["lucide-ripple", "ark-ripple"]
24
+ },
25
+ "select": {
26
+ "files": ["Select.ripple"],
27
+ "path": "components/select",
21
28
  "dependencies": ["utils"],
22
29
  "npmDependencies": ["lucide-ripple", "ark-ripple"]
23
30
  },
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "properties": {
5
+ "$schema": { "type": "string" }
6
+ },
7
+ "additionalProperties": {
8
+ "type": "object",
9
+ "required": ["files", "path"],
10
+ "properties": {
11
+ "files": {
12
+ "type": "array",
13
+ "items": { "type": "string" },
14
+ "minItems": 1
15
+ },
16
+ "path": { "type": "string" },
17
+ "dependencies": {
18
+ "type": "array",
19
+ "items": { "type": "string" }
20
+ },
21
+ "npmDependencies": {
22
+ "type": "array",
23
+ "items": { "type": "string" }
24
+ },
25
+ "target": { "type": "string" }
26
+ },
27
+ "additionalProperties": false
28
+ }
29
+ }