create-bl-theme 1.0.3 → 1.0.5

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
@@ -34,17 +34,26 @@ The CLI will prompt you for:
34
34
  ### Validate a theme
35
35
 
36
36
  ```bash
37
- create-bl-theme validate [directory]
37
+ # Validate a local directory
38
+ create-bl-theme validate ./my-theme
39
+
40
+ # Validate directly from a GitHub repository
41
+ create-bl-theme validate https://github.com/username/theme-repo
38
42
  ```
39
43
 
40
- Checks that your theme has all required files and valid metadata.
44
+ The validator checks:
45
+ - Required files (metadata.json, style.rics or style.css, images/)
46
+ - RICS syntax validation (for .rics files)
47
+ - Valid JSON structure and required fields
48
+ - Image integrity (detects corrupted files)
49
+ - Image dimensions (recommends 1280x720, but other sizes work fine)
41
50
 
42
51
  ## Generated Structure
43
52
 
44
53
  ```
45
54
  my-theme/
46
55
  ├── metadata.json # Theme metadata (required)
47
- ├── style.css # Your CSS styles (required)
56
+ ├── style.rics # Your styles in RICS format (required)
48
57
  ├── DESCRIPTION.md # Rich description (optional, takes precedence)
49
58
  ├── shader.json # Shader config (if enabled)
50
59
  ├── README.md # Theme documentation
@@ -52,6 +61,33 @@ my-theme/
52
61
  └── preview.png
53
62
  ```
54
63
 
64
+ ## RICS
65
+
66
+ Themes use [RICS](https://github.com/better-lyrics/rics) - a lightweight CSS preprocessor with full CSS parity. RICS adds variables, nesting, and mixins while staying close to standard CSS.
67
+
68
+ **Any valid CSS is also valid RICS**, so you can write plain CSS if you prefer.
69
+
70
+ ```scss
71
+ $accent: #ff6b6b;
72
+
73
+ .lyrics-container {
74
+ background: rgba(0, 0, 0, 0.8);
75
+
76
+ .lyrics-line {
77
+ color: $accent;
78
+
79
+ &.active {
80
+ font-weight: bold;
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ - **Playground:** https://rics.boidu.dev
87
+ - **RICS Docs:** https://github.com/better-lyrics/rics
88
+
89
+ > **Note:** Both `.rics` and `.css` files are supported. The validator prefers `.rics` if both exist.
90
+
55
91
  ### Theme Description Options
56
92
 
57
93
  You can provide your theme description in two ways:
@@ -69,7 +105,7 @@ Better Lyrics supports **GitHub Flavored Markdown (GFM)** in DESCRIPTION.md, so
69
105
 
70
106
  ## Theme Development
71
107
 
72
- 1. **Edit `style.css`** - Add your custom styles. Use browser DevTools to inspect Better Lyrics elements and find the right selectors.
108
+ 1. **Edit `style.rics`** - Add your custom styles using RICS or plain CSS. Use browser DevTools to inspect Better Lyrics elements and find the right selectors.
73
109
 
74
110
  2. **Add screenshots** - Place at least one preview image in `images/`. Recommended size: 1280x720 (16:9 aspect ratio).
75
111
 
@@ -77,10 +113,13 @@ Better Lyrics supports **GitHub Flavored Markdown (GFM)** in DESCRIPTION.md, so
77
113
 
78
114
  4. **Test locally** - Install your theme via "Install from URL" in Better Lyrics using your local path or GitHub repo URL.
79
115
 
116
+ **Resources:**
117
+ - [Styling Guide](https://github.com/better-lyrics/better-lyrics/blob/master/STYLING.md) - Available selectors and styling reference
118
+
80
119
  ## Submitting to Theme Store
81
120
 
82
121
  1. Push your theme to a GitHub repository
83
- 2. Fork [better-lyrics-themes](https://github.com/better-lyrics/better-lyrics-themes)
122
+ 2. Fork [themes](https://github.com/better-lyrics/themes)
84
123
  3. Add your theme to `index.json`:
85
124
  ```json
86
125
  {
package/bin/cli.js CHANGED
@@ -4,13 +4,24 @@ import prompts from "prompts";
4
4
  import pc from "picocolors";
5
5
  import fs from "fs";
6
6
  import path from "path";
7
+ import os from "os";
7
8
  import { fileURLToPath } from "url";
9
+ import { imageSize } from "image-size";
10
+ import { execSync } from "child_process";
11
+ import { compileWithDetails } from "rics";
8
12
 
9
13
  const __filename = fileURLToPath(import.meta.url);
10
14
  const __dirname = path.dirname(__filename);
11
15
 
12
16
  const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
13
17
 
18
+ // Recommended image dimensions
19
+ const RECOMMENDED_WIDTH = 1280;
20
+ const RECOMMENDED_HEIGHT = 720;
21
+
22
+ // GitHub URL patterns
23
+ const GITHUB_URL_PATTERN = /^(?:https?:\/\/)?(?:www\.)?github\.com\/([^\/]+)\/([^\/]+)(?:\/)?(?:\.git)?$/;
24
+
14
25
  async function main() {
15
26
  const args = process.argv.slice(2);
16
27
  const command = args[0];
@@ -35,12 +46,13 @@ async function main() {
35
46
 
36
47
  function showHelp() {
37
48
  console.log(`${pc.bold("Usage:")}
38
- ${pc.cyan("create-bl-theme")} [name] Create a new theme
39
- ${pc.cyan("create-bl-theme")} validate [dir] Validate a theme directory
49
+ ${pc.cyan("create-bl-theme")} [name] Create a new theme
50
+ ${pc.cyan("create-bl-theme")} validate [dir|url] Validate a theme (local or GitHub)
40
51
 
41
52
  ${pc.bold("Examples:")}
42
53
  ${pc.dim("$")} create-bl-theme my-awesome-theme
43
54
  ${pc.dim("$")} create-bl-theme validate ./my-theme
55
+ ${pc.dim("$")} create-bl-theme validate https://github.com/user/theme-repo
44
56
  `);
45
57
  }
46
58
 
@@ -201,12 +213,12 @@ Any special instructions for using this theme.
201
213
  fs.writeFileSync(path.join(fullPath, "DESCRIPTION.md"), descriptionMd);
202
214
  }
203
215
 
204
- // Create style.css from template
205
- const cssTemplate = fs.readFileSync(
206
- path.join(TEMPLATES_DIR, "style.css"),
216
+ // Create style.rics from template
217
+ const ricsTemplate = fs.readFileSync(
218
+ path.join(TEMPLATES_DIR, "style.rics"),
207
219
  "utf-8"
208
220
  );
209
- fs.writeFileSync(path.join(fullPath, "style.css"), cssTemplate);
221
+ fs.writeFileSync(path.join(fullPath, "style.rics"), ricsTemplate);
210
222
 
211
223
  // Create shader.json if needed
212
224
  if (response.hasShaders) {
@@ -250,7 +262,7 @@ MIT
250
262
  console.log();
251
263
  console.log(pc.bold(" Next steps:"));
252
264
  console.log(` ${pc.dim("1.")} cd ${dir}`);
253
- console.log(` ${pc.dim("2.")} Edit ${pc.cyan("style.css")} with your theme styles`);
265
+ console.log(` ${pc.dim("2.")} Edit ${pc.cyan("style.rics")} with your theme styles`);
254
266
  console.log(
255
267
  ` ${pc.dim("3.")} Add a preview screenshot to ${pc.cyan("images/preview.png")}`
256
268
  );
@@ -266,9 +278,15 @@ MIT
266
278
  console.log(
267
279
  ` ${pc.dim(`${stepNum}.`)} Push to GitHub and submit to the theme store`
268
280
  );
269
- console.log(
270
- ` ${pc.dim("https://github.com/boidushya/better-lyrics-themes")}`
271
- );
281
+ console.log();
282
+ console.log(pc.bold(" Resources:"));
283
+ console.log(` ${pc.cyan("RICS")} is a lightweight CSS preprocessor with full CSS parity.`);
284
+ console.log(` It adds variables, nesting, and mixins - but plain CSS works too!`);
285
+ console.log();
286
+ console.log(` ${pc.dim("Playground:")} https://rics.boidu.dev`);
287
+ console.log(` ${pc.dim("RICS Docs:")} https://github.com/better-lyrics/rics`);
288
+ console.log(` ${pc.dim("Styling Guide:")} https://github.com/better-lyrics/better-lyrics/blob/master/STYLING.md`);
289
+ console.log(` ${pc.dim("Submit Theme:")} https://github.com/better-lyrics/themes`);
272
290
  console.log();
273
291
  console.log(
274
292
  pc.dim(" Validate your theme with: ") + pc.cyan(`npx create-bl-theme@latest validate ${dir}`)
@@ -277,10 +295,45 @@ MIT
277
295
  }
278
296
 
279
297
  async function validate(dir) {
280
- const fullPath = path.resolve(process.cwd(), dir);
298
+ let fullPath;
299
+ let tempDir = null;
281
300
  let errors = [];
282
301
  let warnings = [];
283
302
 
303
+ // Check if input is a GitHub URL
304
+ const githubMatch = dir.match(GITHUB_URL_PATTERN);
305
+
306
+ if (githubMatch) {
307
+ const [, owner, repo] = githubMatch;
308
+ const repoUrl = `https://github.com/${owner}/${repo}.git`;
309
+
310
+ console.log(pc.dim(`Cloning ${pc.cyan(`${owner}/${repo}`)} from GitHub...\n`));
311
+
312
+ try {
313
+ // Create temp directory
314
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "bl-theme-"));
315
+ fullPath = tempDir;
316
+
317
+ // Clone the repository (shallow clone for speed)
318
+ execSync(`git clone --depth 1 ${repoUrl} "${tempDir}"`, {
319
+ stdio: "pipe",
320
+ });
321
+
322
+ console.log(pc.green(` Cloned successfully!\n`));
323
+ } catch (e) {
324
+ console.log(pc.red(`Error: Could not clone repository "${owner}/${repo}"`));
325
+ console.log(pc.dim(` Make sure the repository exists and is publicly accessible.\n`));
326
+
327
+ if (e.message) {
328
+ console.log(pc.dim(` ${e.message}`));
329
+ }
330
+
331
+ process.exit(1);
332
+ }
333
+ } else {
334
+ fullPath = path.resolve(process.cwd(), dir);
335
+ }
336
+
284
337
  console.log(pc.dim(`Validating theme at ${fullPath}...\n`));
285
338
 
286
339
  // Check directory exists
@@ -344,6 +397,19 @@ async function validate(dir) {
344
397
  errors.push("metadata.json: images must be an array");
345
398
  }
346
399
 
400
+ // Check if images referenced in metadata.json exist in images/
401
+ if (metadata.images && Array.isArray(metadata.images)) {
402
+ const imagesDir = path.join(fullPath, "images");
403
+ for (const image of metadata.images) {
404
+ const imagePath = path.join(imagesDir, image);
405
+ if (!fs.existsSync(imagePath)) {
406
+ errors.push(
407
+ `metadata.json: image "${image}" not found in images/ directory`
408
+ );
409
+ }
410
+ }
411
+ }
412
+
347
413
  if (!metadata.tags) {
348
414
  warnings.push("metadata.json: consider adding tags for discoverability");
349
415
  }
@@ -352,12 +418,41 @@ async function validate(dir) {
352
418
  }
353
419
  }
354
420
 
355
- // Check style.css
356
- const stylePath = path.join(fullPath, "style.css");
357
- if (!fs.existsSync(stylePath)) {
358
- errors.push("style.css is missing");
359
- } else {
360
- const css = fs.readFileSync(stylePath, "utf-8");
421
+ // Check for style.rics or style.css (prefer .rics)
422
+ const ricsPath = path.join(fullPath, "style.rics");
423
+ const cssPath = path.join(fullPath, "style.css");
424
+ const hasRics = fs.existsSync(ricsPath);
425
+ const hasCss = fs.existsSync(cssPath);
426
+
427
+ if (!hasRics && !hasCss) {
428
+ errors.push("Missing required file: style.rics or style.css");
429
+ } else if (hasRics) {
430
+ const ricsSource = fs.readFileSync(ricsPath, "utf-8");
431
+ if (ricsSource.trim().length === 0) {
432
+ warnings.push("style.rics is empty");
433
+ } else {
434
+ // Validate RICS syntax
435
+ try {
436
+ const result = compileWithDetails(ricsSource);
437
+ if (result.errors && result.errors.length > 0) {
438
+ for (const err of result.errors) {
439
+ const location = err.start
440
+ ? ` (line ${err.start.line}, column ${err.start.column})`
441
+ : "";
442
+ errors.push(`style.rics: ${err.message}${location}`);
443
+ }
444
+ }
445
+ if (result.warnings && result.warnings.length > 0) {
446
+ for (const warn of result.warnings) {
447
+ warnings.push(`style.rics: ${warn.message || warn}`);
448
+ }
449
+ }
450
+ } catch (e) {
451
+ errors.push(`style.rics: Failed to compile - ${e.message}`);
452
+ }
453
+ }
454
+ } else if (hasCss) {
455
+ const css = fs.readFileSync(cssPath, "utf-8");
361
456
  if (css.trim().length === 0) {
362
457
  warnings.push("style.css is empty");
363
458
  }
@@ -373,6 +468,45 @@ async function validate(dir) {
373
468
  .filter((f) => /\.(png|jpg|jpeg|gif|webp)$/i.test(f));
374
469
  if (images.length === 0) {
375
470
  errors.push("images/ directory must contain at least one image");
471
+ } else {
472
+ // Validate each image
473
+ for (const image of images) {
474
+ const imagePath = path.join(imagesDir, image);
475
+
476
+ try {
477
+ const imageBuffer = fs.readFileSync(imagePath);
478
+ const dimensions = imageSize(imageBuffer);
479
+
480
+ if (!dimensions || !dimensions.width || !dimensions.height) {
481
+ errors.push(
482
+ `${pc.bold(image)}: Unable to read image dimensions - the file may be corrupted or in an unsupported format`
483
+ );
484
+ continue;
485
+ }
486
+
487
+ const { width, height } = dimensions;
488
+ const aspectRatio = (width / height).toFixed(2);
489
+ const recommendedAspectRatio = (RECOMMENDED_WIDTH / RECOMMENDED_HEIGHT).toFixed(2);
490
+
491
+ // Only warn if aspect ratio differs from recommended 16:9
492
+ if (aspectRatio !== recommendedAspectRatio) {
493
+ warnings.push(`${pc.bold(image)}: ${width}x${height} (aspect ratio ${aspectRatio})`);
494
+ }
495
+ } catch (e) {
496
+ // Handle corrupted or unreadable images
497
+ if (e.message.includes("unsupported") || e.message.includes("Invalid")) {
498
+ errors.push(
499
+ `${pc.bold(image)}: This image appears to be corrupted or in an unsupported format\n ${pc.dim("Please ensure the file is a valid image (PNG, JPG, GIF, or WebP)")}`
500
+ );
501
+ } else if (e.code === "ENOENT") {
502
+ errors.push(`${pc.bold(image)}: File not found`);
503
+ } else {
504
+ errors.push(
505
+ `${pc.bold(image)}: Could not validate image - ${e.message}\n ${pc.dim("The file may be corrupted or inaccessible")}`
506
+ );
507
+ }
508
+ }
509
+ }
376
510
  }
377
511
  }
378
512
 
@@ -401,12 +535,25 @@ async function validate(dir) {
401
535
  }
402
536
  if (warnings.length > 0) {
403
537
  console.log(pc.yellow(pc.bold("\n Warnings:")));
538
+ console.log(pc.yellow(" Non-standard aspect ratios (recommended: 16:9)"));
404
539
  warnings.forEach((w) => console.log(pc.yellow(` - ${w}`)));
540
+ console.log();
541
+ console.log(pc.dim(pc.yellow(" This is just a suggestion - your images will still work fine!")));
542
+ console.log(pc.dim(pc.yellow(" Different aspect ratios can be intentional for your theme's design.")));
405
543
  }
406
544
  }
407
545
 
408
546
  console.log();
409
547
 
548
+ // Cleanup temp directory if we cloned from GitHub
549
+ if (tempDir) {
550
+ try {
551
+ fs.rmSync(tempDir, { recursive: true, force: true });
552
+ } catch (e) {
553
+ // Ignore cleanup errors
554
+ }
555
+ }
556
+
410
557
  if (errors.length > 0) {
411
558
  process.exit(1);
412
559
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-bl-theme",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "CLI tool to scaffold Better Lyrics themes",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,9 @@
18
18
  "author": "boidushya",
19
19
  "license": "MIT",
20
20
  "dependencies": {
21
+ "image-size": "^2.0.2",
22
+ "picocolors": "^1.1.1",
21
23
  "prompts": "^2.4.2",
22
- "picocolors": "^1.1.1"
24
+ "rics": "^0.3.7"
23
25
  }
24
26
  }
@@ -0,0 +1,11 @@
1
+ /*
2
+ * Better Lyrics Theme
3
+ *
4
+ * This file uses RICS - a lightweight CSS preprocessor with full CSS parity.
5
+ * RICS adds variables, nesting, and mixins while staying close to standard CSS.
6
+ * Any valid CSS is also valid RICS, so you can write plain CSS if you prefer.
7
+ *
8
+ * RICS Playground: https://rics.boidu.dev
9
+ * RICS Docs: https://github.com/better-lyrics/rics
10
+ * Styling Guide: https://github.com/better-lyrics/better-lyrics/blob/master/STYLING.md
11
+ */
@@ -1,6 +0,0 @@
1
- /*
2
- * Better Lyrics Theme
3
- *
4
- * For available selectors and styling guide, see:
5
- * https://github.com/better-lyrics/better-lyrics/blob/master/STYLING.md
6
- */