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 +44 -5
- package/bin/cli.js +164 -17
- package/package.json +4 -2
- package/templates/style.rics +11 -0
- package/templates/style.css +0 -6
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 [
|
|
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]
|
|
39
|
-
${pc.cyan("create-bl-theme")} validate [dir]
|
|
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.
|
|
205
|
-
const
|
|
206
|
-
path.join(TEMPLATES_DIR, "style.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
+
"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
|
-
"
|
|
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
|
+
*/
|