stera-ui 0.1.3 → 0.2.1
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/dist/{chunk-57DNZ2YS.js → chunk-AXIACJ2C.js} +5 -1
- package/dist/{chunk-HCHFPFTS.js → chunk-KIEC5SPW.js} +23 -1
- package/dist/cli.js +510 -33
- package/dist/index.js +2 -2
- package/dist/schema/index.d.ts +18 -0
- package/dist/schema/index.js +1 -1
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +1 -1
- package/package.json +2 -1
|
@@ -34,7 +34,11 @@ var configSchema = z.object({
|
|
|
34
34
|
ui: z.string(),
|
|
35
35
|
lib: z.string(),
|
|
36
36
|
hooks: z.string()
|
|
37
|
-
})
|
|
37
|
+
}),
|
|
38
|
+
fonts: z.object({
|
|
39
|
+
strategy: z.enum(["stera-default", "keep-existing", "skip"]),
|
|
40
|
+
nextFont: z.boolean().optional()
|
|
41
|
+
}).optional()
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
export {
|
|
@@ -165,7 +165,22 @@ function detectProject(cwd) {
|
|
|
165
165
|
rsc = fs2.existsSync(path3.join(cwd, appDir));
|
|
166
166
|
}
|
|
167
167
|
const existingCssFile = findTailwindCssFile(cwd);
|
|
168
|
-
|
|
168
|
+
let layoutFile = null;
|
|
169
|
+
if (framework === "next") {
|
|
170
|
+
const appDir = hasSrc ? "src/app" : "app";
|
|
171
|
+
for (const ext of ["tsx", "jsx", "ts", "js"]) {
|
|
172
|
+
const candidate = `${appDir}/layout.${ext}`;
|
|
173
|
+
if (fs2.existsSync(path3.join(cwd, candidate))) {
|
|
174
|
+
layoutFile = candidate;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else if (framework === "vite") {
|
|
179
|
+
if (fs2.existsSync(path3.join(cwd, "index.html"))) {
|
|
180
|
+
layoutFile = "index.html";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { hasSrc, framework, rsc, existingCssFile, layoutFile };
|
|
169
184
|
}
|
|
170
185
|
|
|
171
186
|
// src/utils/detect-globals.ts
|
|
@@ -287,6 +302,13 @@ async function installDependencies(deps, cwd) {
|
|
|
287
302
|
spinner.succeed(`Installed with ${pm}`);
|
|
288
303
|
} catch (error) {
|
|
289
304
|
spinner.fail(`Install failed`);
|
|
305
|
+
if (error instanceof Error && "stdout" in error && typeof error.stdout === "string" && error.stdout.includes("ERR_PNPM_ADDING_TO_ROOT")) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`Cannot install dependencies at a pnpm workspace root.
|
|
308
|
+
Run stera-ui init from within a specific workspace package instead,
|
|
309
|
+
or use "pnpm add -w <package>" manually to install at the root.`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
290
312
|
throw error;
|
|
291
313
|
}
|
|
292
314
|
}
|
package/dist/cli.js
CHANGED
|
@@ -286,6 +286,13 @@ async function installDependencies(deps, cwd) {
|
|
|
286
286
|
spinner.succeed(`Installed with ${pm}`);
|
|
287
287
|
} catch (error) {
|
|
288
288
|
spinner.fail(`Install failed`);
|
|
289
|
+
if (error instanceof Error && "stdout" in error && typeof error.stdout === "string" && error.stdout.includes("ERR_PNPM_ADDING_TO_ROOT")) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`Cannot install dependencies at a pnpm workspace root.
|
|
292
|
+
Run stera-ui init from within a specific workspace package instead,
|
|
293
|
+
or use "pnpm add -w <package>" manually to install at the root.`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
289
296
|
throw error;
|
|
290
297
|
}
|
|
291
298
|
}
|
|
@@ -444,8 +451,9 @@ async function add(components, options) {
|
|
|
444
451
|
}
|
|
445
452
|
|
|
446
453
|
// src/commands/init.ts
|
|
447
|
-
import
|
|
448
|
-
import
|
|
454
|
+
import fs11 from "fs";
|
|
455
|
+
import path12 from "path";
|
|
456
|
+
import { select, confirm } from "@inquirer/prompts";
|
|
449
457
|
|
|
450
458
|
// src/utils/detect-project.ts
|
|
451
459
|
import fs5 from "fs";
|
|
@@ -486,11 +494,375 @@ function detectProject(cwd) {
|
|
|
486
494
|
rsc = fs5.existsSync(path7.join(cwd, appDir));
|
|
487
495
|
}
|
|
488
496
|
const existingCssFile = findTailwindCssFile(cwd);
|
|
489
|
-
|
|
497
|
+
let layoutFile = null;
|
|
498
|
+
if (framework === "next") {
|
|
499
|
+
const appDir = hasSrc ? "src/app" : "app";
|
|
500
|
+
for (const ext of ["tsx", "jsx", "ts", "js"]) {
|
|
501
|
+
const candidate = `${appDir}/layout.${ext}`;
|
|
502
|
+
if (fs5.existsSync(path7.join(cwd, candidate))) {
|
|
503
|
+
layoutFile = candidate;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
} else if (framework === "vite") {
|
|
508
|
+
if (fs5.existsSync(path7.join(cwd, "index.html"))) {
|
|
509
|
+
layoutFile = "index.html";
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return { hasSrc, framework, rsc, existingCssFile, layoutFile };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/utils/detect-fonts.ts
|
|
516
|
+
import fs6 from "fs";
|
|
517
|
+
import path8 from "path";
|
|
518
|
+
function readIfExists(filePath) {
|
|
519
|
+
try {
|
|
520
|
+
return fs6.readFileSync(filePath, "utf-8");
|
|
521
|
+
} catch {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function detectNextFont(layoutContent) {
|
|
526
|
+
const hasNextFont = /import.*from\s+['"]next\/font/.test(layoutContent) || /import.*from\s+['"]@next\/font/.test(layoutContent) || /import.*from\s+['"]geist\/font/.test(layoutContent);
|
|
527
|
+
let nextFontVariable = null;
|
|
528
|
+
const varMatch = layoutContent.match(/variable:\s*['"](--.+?)['"]/);
|
|
529
|
+
if (varMatch) {
|
|
530
|
+
nextFontVariable = varMatch[1];
|
|
531
|
+
}
|
|
532
|
+
return { hasNextFont, nextFontVariable };
|
|
533
|
+
}
|
|
534
|
+
function detectFontFace(cssContent) {
|
|
535
|
+
const hasFontFace = /@font-face\s*\{/.test(cssContent);
|
|
536
|
+
const fontFamilies = [];
|
|
537
|
+
const regex = /font-family:\s*['"]?([^'";}\n]+?)['"]?\s*;/g;
|
|
538
|
+
let match;
|
|
539
|
+
while ((match = regex.exec(cssContent)) !== null) {
|
|
540
|
+
const family = match[1].trim();
|
|
541
|
+
if (family && !fontFamilies.includes(family)) {
|
|
542
|
+
fontFamilies.push(family);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return { hasFontFace, fontFamilies };
|
|
546
|
+
}
|
|
547
|
+
function detectGoogleFonts(content) {
|
|
548
|
+
return content.includes("fonts.googleapis.com") || content.includes("fonts.gstatic.com");
|
|
549
|
+
}
|
|
550
|
+
function detectFonts(cwd, project) {
|
|
551
|
+
const result = {
|
|
552
|
+
hasNextFont: false,
|
|
553
|
+
hasFontFace: false,
|
|
554
|
+
hasGoogleFontsLink: false,
|
|
555
|
+
fontFamilies: [],
|
|
556
|
+
nextFontVariable: null
|
|
557
|
+
};
|
|
558
|
+
if (project.layoutFile) {
|
|
559
|
+
const layoutContent = readIfExists(path8.join(cwd, project.layoutFile));
|
|
560
|
+
if (layoutContent) {
|
|
561
|
+
const nextFontResult = detectNextFont(layoutContent);
|
|
562
|
+
result.hasNextFont = nextFontResult.hasNextFont;
|
|
563
|
+
result.nextFontVariable = nextFontResult.nextFontVariable;
|
|
564
|
+
if (detectGoogleFonts(layoutContent)) {
|
|
565
|
+
result.hasGoogleFontsLink = true;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (project.existingCssFile) {
|
|
570
|
+
const cssContent = readIfExists(path8.join(cwd, project.existingCssFile));
|
|
571
|
+
if (cssContent) {
|
|
572
|
+
const fontFaceResult = detectFontFace(cssContent);
|
|
573
|
+
result.hasFontFace = fontFaceResult.hasFontFace;
|
|
574
|
+
result.fontFamilies.push(...fontFaceResult.fontFamilies);
|
|
575
|
+
if (detectGoogleFonts(cssContent)) {
|
|
576
|
+
result.hasGoogleFontsLink = true;
|
|
577
|
+
}
|
|
578
|
+
const importRegex = /@import\s+['"](.+?)['"]/g;
|
|
579
|
+
let importMatch;
|
|
580
|
+
while ((importMatch = importRegex.exec(cssContent)) !== null) {
|
|
581
|
+
const importPath = importMatch[1];
|
|
582
|
+
const resolvedPath = path8.join(
|
|
583
|
+
cwd,
|
|
584
|
+
path8.dirname(project.existingCssFile),
|
|
585
|
+
importPath
|
|
586
|
+
);
|
|
587
|
+
const importedContent = readIfExists(resolvedPath);
|
|
588
|
+
if (importedContent) {
|
|
589
|
+
const imported = detectFontFace(importedContent);
|
|
590
|
+
if (imported.hasFontFace) result.hasFontFace = true;
|
|
591
|
+
for (const family of imported.fontFamilies) {
|
|
592
|
+
if (!result.fontFamilies.includes(family)) {
|
|
593
|
+
result.fontFamilies.push(family);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
for (const htmlFile of ["index.html", "src/index.html"]) {
|
|
601
|
+
const htmlContent = readIfExists(path8.join(cwd, htmlFile));
|
|
602
|
+
if (htmlContent && detectGoogleFonts(htmlContent)) {
|
|
603
|
+
result.hasGoogleFontsLink = true;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return result;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/utils/patch-css-vars.ts
|
|
610
|
+
import fs7 from "fs";
|
|
611
|
+
function patchCssVariables(filePath, patches) {
|
|
612
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
613
|
+
const lines = content.split("\n");
|
|
614
|
+
const patched = [];
|
|
615
|
+
for (let i = 0; i < lines.length; i++) {
|
|
616
|
+
for (const [prop, newValue] of Object.entries(patches)) {
|
|
617
|
+
const regex = new RegExp(`(\\s*${escapeRegex(prop)}:\\s*).+?(;)`);
|
|
618
|
+
if (regex.test(lines[i])) {
|
|
619
|
+
lines[i] = lines[i].replace(regex, `$1${newValue}$2`);
|
|
620
|
+
if (!patched.includes(prop)) patched.push(prop);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (patched.length > 0) {
|
|
625
|
+
fs7.writeFileSync(filePath, lines.join("\n"), "utf-8");
|
|
626
|
+
}
|
|
627
|
+
return patched;
|
|
628
|
+
}
|
|
629
|
+
function removeCssImport(filePath, importPath) {
|
|
630
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
631
|
+
const pattern = new RegExp(
|
|
632
|
+
`@import\\s+['"]${escapeRegex(importPath)}['"]\\s*;?\\n?`
|
|
633
|
+
);
|
|
634
|
+
if (!pattern.test(content)) return false;
|
|
635
|
+
fs7.writeFileSync(filePath, content.replace(pattern, ""), "utf-8");
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
function escapeRegex(str) {
|
|
639
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/utils/setup-fonts-generic.ts
|
|
643
|
+
import fs9 from "fs";
|
|
644
|
+
import path10 from "path";
|
|
645
|
+
|
|
646
|
+
// src/utils/copy-fonts.ts
|
|
647
|
+
import fs8 from "fs";
|
|
648
|
+
import path9 from "path";
|
|
649
|
+
import { createRequire } from "module";
|
|
650
|
+
var FONT_FILES = [
|
|
651
|
+
{
|
|
652
|
+
src: "geist-sans/Geist-Variable.woff2",
|
|
653
|
+
dest: "geist/Geist-Variable.woff2"
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
src: "geist-sans/Geist-Italic[wght].woff2",
|
|
657
|
+
dest: "geist/Geist-Italic.woff2"
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
src: "geist-mono/GeistMono-Variable.woff2",
|
|
661
|
+
dest: "geist-mono/GeistMono-Variable.woff2"
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
src: "geist-mono/GeistMono-Italic[wght].woff2",
|
|
665
|
+
dest: "geist-mono/GeistMono-Italic.woff2"
|
|
666
|
+
}
|
|
667
|
+
];
|
|
668
|
+
function resolveGeistFontsDir(cwd) {
|
|
669
|
+
const require2 = createRequire(path9.join(cwd, "package.json"));
|
|
670
|
+
const geistEntry = require2.resolve("geist/font/sans");
|
|
671
|
+
const distDir = path9.dirname(geistEntry);
|
|
672
|
+
return path9.join(distDir, "fonts");
|
|
673
|
+
}
|
|
674
|
+
function copyFonts(cwd) {
|
|
675
|
+
const spinner = createSpinner("Copying font files");
|
|
676
|
+
const fontsDir = resolveGeistFontsDir(cwd);
|
|
677
|
+
const publicFontsDir = path9.join(cwd, "public", "fonts");
|
|
678
|
+
const written = [];
|
|
679
|
+
for (const file of FONT_FILES) {
|
|
680
|
+
const srcPath = path9.join(fontsDir, file.src);
|
|
681
|
+
const destPath = path9.join(publicFontsDir, file.dest);
|
|
682
|
+
if (fs8.existsSync(destPath)) continue;
|
|
683
|
+
if (!fs8.existsSync(srcPath)) {
|
|
684
|
+
spinner.fail(`Font file not found: ${file.src}`);
|
|
685
|
+
throw new Error(
|
|
686
|
+
`Could not find ${srcPath}. Is the geist package installed?`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
fs8.mkdirSync(path9.dirname(destPath), { recursive: true });
|
|
690
|
+
fs8.copyFileSync(srcPath, destPath);
|
|
691
|
+
written.push(path9.relative(cwd, destPath));
|
|
692
|
+
}
|
|
693
|
+
if (written.length > 0) {
|
|
694
|
+
spinner.succeed(`Copied ${written.length} font files to public/fonts/`);
|
|
695
|
+
} else {
|
|
696
|
+
spinner.succeed("Font files already exist");
|
|
697
|
+
}
|
|
698
|
+
return written;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/utils/setup-fonts-generic.ts
|
|
702
|
+
var FONTS_CSS_CONTENT = `/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
703
|
+
Stera UI font registry
|
|
704
|
+
To add a font:
|
|
705
|
+
1. Drop .woff2 file(s) into public/fonts/{family-name}/
|
|
706
|
+
2. Add an @font-face block below
|
|
707
|
+
3. Add a --font-* variable in globals.css
|
|
708
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
709
|
+
|
|
710
|
+
/* \u2500\u2500\u2500 Geist Sans \u2500\u2500\u2500 */
|
|
711
|
+
@font-face {
|
|
712
|
+
font-family: 'Geist';
|
|
713
|
+
src: url('/fonts/geist/Geist-Variable.woff2') format('woff2');
|
|
714
|
+
font-weight: 100 900;
|
|
715
|
+
font-style: normal;
|
|
716
|
+
font-display: swap;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
@font-face {
|
|
720
|
+
font-family: 'Geist';
|
|
721
|
+
src: url('/fonts/geist/Geist-Italic.woff2') format('woff2');
|
|
722
|
+
font-weight: 100 900;
|
|
723
|
+
font-style: italic;
|
|
724
|
+
font-display: swap;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/* \u2500\u2500\u2500 Geist Mono \u2500\u2500\u2500 */
|
|
728
|
+
@font-face {
|
|
729
|
+
font-family: 'Geist Mono';
|
|
730
|
+
src: url('/fonts/geist-mono/GeistMono-Variable.woff2') format('woff2');
|
|
731
|
+
font-weight: 100 900;
|
|
732
|
+
font-style: normal;
|
|
733
|
+
font-display: swap;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
@font-face {
|
|
737
|
+
font-family: 'Geist Mono';
|
|
738
|
+
src: url('/fonts/geist-mono/GeistMono-Italic.woff2') format('woff2');
|
|
739
|
+
font-weight: 100 900;
|
|
740
|
+
font-style: italic;
|
|
741
|
+
font-display: swap;
|
|
742
|
+
}
|
|
743
|
+
`;
|
|
744
|
+
var PRELOAD_TAGS = `<link rel="preload" href="/fonts/geist/Geist-Variable.woff2" as="font" type="font/woff2" crossorigin />
|
|
745
|
+
<link rel="preload" href="/fonts/geist-mono/GeistMono-Variable.woff2" as="font" type="font/woff2" crossorigin />`;
|
|
746
|
+
function setupFontsGeneric(cwd, cssDir) {
|
|
747
|
+
copyFonts(cwd);
|
|
748
|
+
const fontsPath = path10.join(cssDir, "fonts.css");
|
|
749
|
+
if (!fs9.existsSync(fontsPath)) {
|
|
750
|
+
fs9.mkdirSync(path10.dirname(fontsPath), { recursive: true });
|
|
751
|
+
fs9.writeFileSync(fontsPath, FONTS_CSS_CONTENT, "utf-8");
|
|
752
|
+
console.log(` ${CHECK} Created ${path10.relative(cwd, fontsPath)}`);
|
|
753
|
+
} else {
|
|
754
|
+
console.log(` ${dim(path10.relative(cwd, fontsPath) + " (already exists, skipped)")}`);
|
|
755
|
+
}
|
|
756
|
+
return { preloadTags: PRELOAD_TAGS };
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/utils/setup-fonts-nextjs.ts
|
|
760
|
+
import fs10 from "fs";
|
|
761
|
+
import path11 from "path";
|
|
762
|
+
var GEIST_IMPORTS = `import { GeistSans } from "geist/font/sans"
|
|
763
|
+
import { GeistMono } from "geist/font/mono"`;
|
|
764
|
+
function canPatchLayout(content) {
|
|
765
|
+
if (!/<html[\s>]/.test(content)) return false;
|
|
766
|
+
if ((content.match(/<html[\s>]/g) || []).length > 1) return false;
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
function insertImports(content, imports) {
|
|
770
|
+
const lines = content.split("\n");
|
|
771
|
+
let lastImportIndex = -1;
|
|
772
|
+
for (let i = 0; i < lines.length; i++) {
|
|
773
|
+
const trimmed = lines[i].trim();
|
|
774
|
+
if (trimmed.startsWith("import ") || trimmed.startsWith("import{") || trimmed.startsWith("import ")) {
|
|
775
|
+
lastImportIndex = i;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (lastImportIndex >= 0) {
|
|
779
|
+
lines.splice(lastImportIndex + 1, 0, imports);
|
|
780
|
+
} else {
|
|
781
|
+
lines.unshift(imports);
|
|
782
|
+
}
|
|
783
|
+
return lines.join("\n");
|
|
784
|
+
}
|
|
785
|
+
function addClassNameToHtml(content) {
|
|
786
|
+
const fontClasses = "${GeistSans.variable} ${GeistMono.variable}";
|
|
787
|
+
const templateMatch = content.match(
|
|
788
|
+
/(<html[^>]*className=\{`)(.*?)(`\})/s
|
|
789
|
+
);
|
|
790
|
+
if (templateMatch) {
|
|
791
|
+
const existing = templateMatch[2].trim();
|
|
792
|
+
const newClassName = existing ? `${existing} ${fontClasses}` : fontClasses;
|
|
793
|
+
return content.replace(
|
|
794
|
+
templateMatch[0],
|
|
795
|
+
`${templateMatch[1]}${newClassName}${templateMatch[3]}`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
const exprMatch = content.match(
|
|
799
|
+
/(<html[^>]*className=\{)([^}`]+)(\})/
|
|
800
|
+
);
|
|
801
|
+
if (exprMatch) {
|
|
802
|
+
return content.replace(
|
|
803
|
+
exprMatch[0],
|
|
804
|
+
`${exprMatch[1]}\`\${${exprMatch[2].trim()}} ${fontClasses}\`${exprMatch[3]}`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const stringMatch = content.match(
|
|
808
|
+
/(<html[^>]*className=)(["'])([^"']*)(["'])/
|
|
809
|
+
);
|
|
810
|
+
if (stringMatch) {
|
|
811
|
+
const existing = stringMatch[3].trim();
|
|
812
|
+
const newClassName = existing ? `${existing} ${fontClasses}` : fontClasses;
|
|
813
|
+
return content.replace(
|
|
814
|
+
stringMatch[0],
|
|
815
|
+
`${stringMatch[1]}{\`${newClassName}\`}`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
return content.replace(
|
|
819
|
+
/<html(\s)/,
|
|
820
|
+
`<html className={\`${fontClasses}\`}$1`
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
function generateSnippet() {
|
|
824
|
+
return `
|
|
825
|
+
Add these imports to your root layout file:
|
|
826
|
+
|
|
827
|
+
${dim(GEIST_IMPORTS)}
|
|
828
|
+
|
|
829
|
+
Then add the font variables to your <html> tag:
|
|
830
|
+
|
|
831
|
+
${dim("<html className={`${GeistSans.variable} ${GeistMono.variable}`}>")}
|
|
832
|
+
`;
|
|
833
|
+
}
|
|
834
|
+
function setupFontsNextjs(cwd, layoutFile, cssFile) {
|
|
835
|
+
const layoutPath = path11.join(cwd, layoutFile);
|
|
836
|
+
const cssPath = path11.join(cwd, cssFile);
|
|
837
|
+
let layoutContent = fs10.readFileSync(layoutPath, "utf-8");
|
|
838
|
+
if (layoutContent.includes("geist/font/sans") || layoutContent.includes("GeistSans")) {
|
|
839
|
+
console.log(` ${dim("geist/font already configured in " + layoutFile)}`);
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
if (!canPatchLayout(layoutContent)) {
|
|
843
|
+
console.log(` ${WARN} Could not auto-patch ${layoutFile}`);
|
|
844
|
+
console.log(generateSnippet());
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
layoutContent = insertImports(layoutContent, GEIST_IMPORTS);
|
|
848
|
+
layoutContent = addClassNameToHtml(layoutContent);
|
|
849
|
+
fs10.writeFileSync(layoutPath, layoutContent, "utf-8");
|
|
850
|
+
console.log(` ${CHECK} Updated ${layoutFile} with geist/font imports`);
|
|
851
|
+
if (fs10.existsSync(cssPath)) {
|
|
852
|
+
const patched = patchCssVariables(cssPath, {
|
|
853
|
+
"--font-display": "var(--font-geist-sans)",
|
|
854
|
+
"--font-body": "var(--font-geist-sans)"
|
|
855
|
+
});
|
|
856
|
+
if (patched.length > 0) {
|
|
857
|
+
console.log(` ${CHECK} Updated font variables in ${path11.relative(cwd, cssPath)}`);
|
|
858
|
+
}
|
|
859
|
+
removeCssImport(cssPath, "./fonts.css");
|
|
860
|
+
}
|
|
861
|
+
return true;
|
|
490
862
|
}
|
|
491
863
|
|
|
492
864
|
// src/commands/init.ts
|
|
493
|
-
function buildConfig(project) {
|
|
865
|
+
function buildConfig(project, fontChoice) {
|
|
494
866
|
let cssPath;
|
|
495
867
|
if (project.existingCssFile) {
|
|
496
868
|
cssPath = project.existingCssFile;
|
|
@@ -509,6 +881,10 @@ function buildConfig(project) {
|
|
|
509
881
|
ui: "@/components/ui",
|
|
510
882
|
lib: "@/lib",
|
|
511
883
|
hooks: "@/hooks"
|
|
884
|
+
},
|
|
885
|
+
fonts: {
|
|
886
|
+
strategy: fontChoice.strategy,
|
|
887
|
+
...fontChoice.useNextFont ? { nextFont: true } : {}
|
|
512
888
|
}
|
|
513
889
|
};
|
|
514
890
|
}
|
|
@@ -516,7 +892,7 @@ function stripTailwindCoreImport(content) {
|
|
|
516
892
|
return content.split("\n").filter((line) => !line.trim().match(/^@import\s+["']tailwindcss["']/)).join("\n");
|
|
517
893
|
}
|
|
518
894
|
function insertImportLine(filePath, importStatement) {
|
|
519
|
-
const content =
|
|
895
|
+
const content = fs11.readFileSync(filePath, "utf-8");
|
|
520
896
|
const lines = content.split("\n");
|
|
521
897
|
let lastImportIndex = -1;
|
|
522
898
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -529,10 +905,100 @@ function insertImportLine(filePath, importStatement) {
|
|
|
529
905
|
} else {
|
|
530
906
|
lines.unshift(importStatement);
|
|
531
907
|
}
|
|
532
|
-
|
|
908
|
+
fs11.writeFileSync(filePath, lines.join("\n"), "utf-8");
|
|
909
|
+
}
|
|
910
|
+
async function promptFontChoice(project, detected, skipPrompts) {
|
|
911
|
+
const hasExistingFonts = detected.hasNextFont || detected.hasFontFace || detected.hasGoogleFontsLink;
|
|
912
|
+
const defaultStrategy = hasExistingFonts ? "keep-existing" : "stera-default";
|
|
913
|
+
if (skipPrompts) {
|
|
914
|
+
return {
|
|
915
|
+
strategy: defaultStrategy,
|
|
916
|
+
useNextFont: project.framework === "next" && defaultStrategy === "stera-default"
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
if (hasExistingFonts) {
|
|
920
|
+
const sources = [];
|
|
921
|
+
if (detected.fontFamilies.length > 0) {
|
|
922
|
+
sources.push(detected.fontFamilies.join(", "));
|
|
923
|
+
}
|
|
924
|
+
if (detected.hasNextFont) sources.push("next/font");
|
|
925
|
+
if (detected.hasGoogleFontsLink) sources.push("Google Fonts");
|
|
926
|
+
console.log(` Detected existing fonts: ${sources.join(" via ")}
|
|
927
|
+
`);
|
|
928
|
+
}
|
|
929
|
+
const choices = [];
|
|
930
|
+
if (hasExistingFonts) {
|
|
931
|
+
choices.push({
|
|
932
|
+
value: "keep-existing",
|
|
933
|
+
name: "Keep my current fonts",
|
|
934
|
+
description: "Update CSS variables to use your existing fonts"
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
choices.push({
|
|
938
|
+
value: "stera-default",
|
|
939
|
+
name: "Use Stera UI default fonts (Geist Sans + Geist Mono)",
|
|
940
|
+
description: "Self-hosted, optimized variable fonts"
|
|
941
|
+
});
|
|
942
|
+
choices.push({
|
|
943
|
+
value: "skip",
|
|
944
|
+
name: "Skip font setup",
|
|
945
|
+
description: "Configure fonts manually later"
|
|
946
|
+
});
|
|
947
|
+
const strategy = await select({
|
|
948
|
+
message: "How would you like to set up fonts?",
|
|
949
|
+
choices,
|
|
950
|
+
default: defaultStrategy
|
|
951
|
+
});
|
|
952
|
+
let useNextFont = false;
|
|
953
|
+
if (project.framework === "next" && strategy === "stera-default") {
|
|
954
|
+
useNextFont = await confirm({
|
|
955
|
+
message: "Use next/font for automatic optimization? (recommended)",
|
|
956
|
+
default: true
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
console.log("");
|
|
960
|
+
return { strategy, useNextFont };
|
|
961
|
+
}
|
|
962
|
+
async function executeFontStrategy(fontChoice, project, detected, config, cwd) {
|
|
963
|
+
const cssDir = path12.dirname(path12.resolve(cwd, config.css));
|
|
964
|
+
const cssPath = path12.resolve(cwd, config.css);
|
|
965
|
+
if (fontChoice.strategy === "skip") {
|
|
966
|
+
return {};
|
|
967
|
+
}
|
|
968
|
+
if (fontChoice.strategy === "keep-existing") {
|
|
969
|
+
if (fs11.existsSync(cssPath)) {
|
|
970
|
+
const fontFamily = detected.fontFamilies.length > 0 ? detected.fontFamilies[0] : null;
|
|
971
|
+
const fontValue = detected.nextFontVariable ? `var(${detected.nextFontVariable})` : fontFamily ?? "Geist";
|
|
972
|
+
const patched = patchCssVariables(cssPath, {
|
|
973
|
+
"--font-display": fontValue,
|
|
974
|
+
"--font-body": fontValue
|
|
975
|
+
});
|
|
976
|
+
const steraUiPath = path12.join(cssDir, "stera-ui.css");
|
|
977
|
+
if (fs11.existsSync(steraUiPath)) {
|
|
978
|
+
patchCssVariables(steraUiPath, {
|
|
979
|
+
"--font-display": fontValue,
|
|
980
|
+
"--font-body": fontValue
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
if (patched.length > 0) {
|
|
984
|
+
console.log(` ${CHECK} Updated font variables to use ${fontValue}`);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return {};
|
|
988
|
+
}
|
|
989
|
+
if (fontChoice.useNextFont && project.layoutFile) {
|
|
990
|
+
await installDependencies(["geist"], cwd);
|
|
991
|
+
const targetCss = fs11.existsSync(path12.join(cssDir, "stera-ui.css")) ? path12.join(cssDir, "stera-ui.css") : cssPath;
|
|
992
|
+
setupFontsNextjs(cwd, project.layoutFile, targetCss);
|
|
993
|
+
return {};
|
|
994
|
+
} else {
|
|
995
|
+
await installDependencies(["geist"], cwd);
|
|
996
|
+
const result = setupFontsGeneric(cwd, cssDir);
|
|
997
|
+
return { preloadTags: result.preloadTags };
|
|
998
|
+
}
|
|
533
999
|
}
|
|
534
1000
|
async function init(options) {
|
|
535
|
-
const cwd = options.cwd ?
|
|
1001
|
+
const cwd = options.cwd ? path12.resolve(options.cwd) : process.cwd();
|
|
536
1002
|
console.log(`
|
|
537
1003
|
${LOGO} Stera UI
|
|
538
1004
|
`);
|
|
@@ -542,8 +1008,8 @@ async function init(options) {
|
|
|
542
1008
|
console.log(' Run "stera-ui add <component>" to add components.\n');
|
|
543
1009
|
return;
|
|
544
1010
|
}
|
|
545
|
-
const pkgPath =
|
|
546
|
-
if (!
|
|
1011
|
+
const pkgPath = path12.join(cwd, "package.json");
|
|
1012
|
+
if (!fs11.existsSync(pkgPath)) {
|
|
547
1013
|
console.error(
|
|
548
1014
|
" Error: No package.json found. Please run this in a project directory."
|
|
549
1015
|
);
|
|
@@ -558,47 +1024,41 @@ async function init(options) {
|
|
|
558
1024
|
console.log(` Detected ${parts.join(" project with ")} project
|
|
559
1025
|
`);
|
|
560
1026
|
}
|
|
561
|
-
const
|
|
562
|
-
const
|
|
563
|
-
|
|
1027
|
+
const detectedFonts = detectFonts(cwd, project);
|
|
1028
|
+
const fontChoice = await promptFontChoice(
|
|
1029
|
+
project,
|
|
1030
|
+
detectedFonts,
|
|
1031
|
+
options.yes ?? false
|
|
1032
|
+
);
|
|
1033
|
+
const config = buildConfig(project, fontChoice);
|
|
1034
|
+
const configPath = path12.join(cwd, CONFIG_FILE);
|
|
1035
|
+
fs11.writeFileSync(
|
|
564
1036
|
configPath,
|
|
565
1037
|
JSON.stringify(config, null, 2) + "\n",
|
|
566
1038
|
"utf-8"
|
|
567
1039
|
);
|
|
568
1040
|
console.log(` ${CHECK} Created components.json`);
|
|
569
|
-
const cssDir =
|
|
1041
|
+
const cssDir = path12.dirname(path12.resolve(cwd, config.css));
|
|
570
1042
|
if (project.existingCssFile) {
|
|
571
1043
|
if (hasGlobalsCss(config, cwd)) {
|
|
572
1044
|
console.log(
|
|
573
|
-
` ${CHECK} Stera already initialised ${dim(`(${
|
|
1045
|
+
` ${CHECK} Stera already initialised ${dim(`(${path12.join(path12.dirname(project.existingCssFile), "stera-ui.css")} exists)`)}`
|
|
574
1046
|
);
|
|
575
1047
|
} else {
|
|
576
1048
|
try {
|
|
577
1049
|
const globalsItem = await fetchRegistryItem("globals");
|
|
578
1050
|
const globalsContent = globalsItem.files[0].content ?? "";
|
|
579
1051
|
const steraUiContent = stripTailwindCoreImport(globalsContent);
|
|
580
|
-
const steraUiPath =
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
console.log(` ${CHECK} Created ${
|
|
584
|
-
const existingCssPath =
|
|
1052
|
+
const steraUiPath = path12.join(cssDir, "stera-ui.css");
|
|
1053
|
+
fs11.mkdirSync(cssDir, { recursive: true });
|
|
1054
|
+
fs11.writeFileSync(steraUiPath, steraUiContent, "utf-8");
|
|
1055
|
+
console.log(` ${CHECK} Created ${path12.relative(cwd, steraUiPath)}`);
|
|
1056
|
+
const existingCssPath = path12.resolve(cwd, project.existingCssFile);
|
|
585
1057
|
insertImportLine(existingCssPath, "@import './stera-ui.css';");
|
|
586
1058
|
console.log(` ${CHECK} Added @import to ${project.existingCssFile}`);
|
|
587
1059
|
} catch {
|
|
588
1060
|
}
|
|
589
1061
|
}
|
|
590
|
-
try {
|
|
591
|
-
const fontsItem = await fetchRegistryItem("fonts");
|
|
592
|
-
const fontsPath = path8.join(cssDir, "fonts.css");
|
|
593
|
-
fs6.mkdirSync(path8.dirname(fontsPath), { recursive: true });
|
|
594
|
-
if (!fs6.existsSync(fontsPath)) {
|
|
595
|
-
fs6.writeFileSync(fontsPath, fontsItem.files[0].content ?? "", "utf-8");
|
|
596
|
-
console.log(` ${CHECK} Created ${path8.relative(cwd, fontsPath)}`);
|
|
597
|
-
} else {
|
|
598
|
-
console.log(` ${dim(path8.relative(cwd, fontsPath) + " (already exists, skipped)")}`);
|
|
599
|
-
}
|
|
600
|
-
} catch {
|
|
601
|
-
}
|
|
602
1062
|
} else {
|
|
603
1063
|
const resolved = await resolveDependencies(["globals"]);
|
|
604
1064
|
const { written } = await writeComponentFiles(resolved, config, cwd);
|
|
@@ -606,6 +1066,13 @@ async function init(options) {
|
|
|
606
1066
|
console.log(` ${CHECK} Created ${file}`);
|
|
607
1067
|
}
|
|
608
1068
|
}
|
|
1069
|
+
const fontResult = await executeFontStrategy(
|
|
1070
|
+
fontChoice,
|
|
1071
|
+
project,
|
|
1072
|
+
detectedFonts,
|
|
1073
|
+
config,
|
|
1074
|
+
cwd
|
|
1075
|
+
);
|
|
609
1076
|
try {
|
|
610
1077
|
const globalsItem = await fetchRegistryItem("globals");
|
|
611
1078
|
if (globalsItem.dependencies) {
|
|
@@ -618,8 +1085,18 @@ async function init(options) {
|
|
|
618
1085
|
console.log("");
|
|
619
1086
|
console.log(" Next steps:");
|
|
620
1087
|
console.log(" stera-ui add <component>");
|
|
621
|
-
|
|
622
|
-
|
|
1088
|
+
if (fontResult.preloadTags) {
|
|
1089
|
+
console.log("");
|
|
1090
|
+
console.log(` ${dim("For optimal font loading, add these to your HTML <head>:")}`);
|
|
1091
|
+
console.log("");
|
|
1092
|
+
for (const line of fontResult.preloadTags.split("\n")) {
|
|
1093
|
+
console.log(` ${dim(line)}`);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (fontChoice.strategy === "skip") {
|
|
1097
|
+
console.log("");
|
|
1098
|
+
console.log(` ${dim("Font setup was skipped. See https://ui.stera.sh for font configuration docs.")}`);
|
|
1099
|
+
}
|
|
623
1100
|
console.log("");
|
|
624
1101
|
}
|
|
625
1102
|
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
registryItemFileSchema,
|
|
12
12
|
registryItemSchema,
|
|
13
13
|
registryItemTypeSchema
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-AXIACJ2C.js";
|
|
15
15
|
import {
|
|
16
16
|
CHECK,
|
|
17
17
|
CONFIG_FILE,
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
resolveOutputPath,
|
|
32
32
|
transformImports,
|
|
33
33
|
writeComponentFiles
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-KIEC5SPW.js";
|
|
35
35
|
export {
|
|
36
36
|
CHECK,
|
|
37
37
|
CONFIG_FILE,
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -163,6 +163,16 @@ declare const configSchema: z.ZodObject<{
|
|
|
163
163
|
lib: string;
|
|
164
164
|
hooks: string;
|
|
165
165
|
}>;
|
|
166
|
+
fonts: z.ZodOptional<z.ZodObject<{
|
|
167
|
+
strategy: z.ZodEnum<["stera-default", "keep-existing", "skip"]>;
|
|
168
|
+
nextFont: z.ZodOptional<z.ZodBoolean>;
|
|
169
|
+
}, "strip", z.ZodTypeAny, {
|
|
170
|
+
strategy: "stera-default" | "keep-existing" | "skip";
|
|
171
|
+
nextFont?: boolean | undefined;
|
|
172
|
+
}, {
|
|
173
|
+
strategy: "stera-default" | "keep-existing" | "skip";
|
|
174
|
+
nextFont?: boolean | undefined;
|
|
175
|
+
}>>;
|
|
166
176
|
}, "strip", z.ZodTypeAny, {
|
|
167
177
|
version: 1;
|
|
168
178
|
css: string;
|
|
@@ -174,6 +184,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
174
184
|
hooks: string;
|
|
175
185
|
};
|
|
176
186
|
$schema?: string | undefined;
|
|
187
|
+
fonts?: {
|
|
188
|
+
strategy: "stera-default" | "keep-existing" | "skip";
|
|
189
|
+
nextFont?: boolean | undefined;
|
|
190
|
+
} | undefined;
|
|
177
191
|
}, {
|
|
178
192
|
version: 1;
|
|
179
193
|
css: string;
|
|
@@ -185,6 +199,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
185
199
|
hooks: string;
|
|
186
200
|
};
|
|
187
201
|
$schema?: string | undefined;
|
|
202
|
+
fonts?: {
|
|
203
|
+
strategy: "stera-default" | "keep-existing" | "skip";
|
|
204
|
+
nextFont?: boolean | undefined;
|
|
205
|
+
} | undefined;
|
|
188
206
|
}>;
|
|
189
207
|
type SteraConfig = z.infer<typeof configSchema>;
|
|
190
208
|
|
package/dist/schema/index.js
CHANGED
package/dist/utils/index.d.ts
CHANGED
|
@@ -12,6 +12,10 @@ interface SteraConfig {
|
|
|
12
12
|
lib: string;
|
|
13
13
|
hooks: string;
|
|
14
14
|
};
|
|
15
|
+
fonts?: {
|
|
16
|
+
strategy: "stera-default" | "keep-existing" | "skip";
|
|
17
|
+
nextFont?: boolean;
|
|
18
|
+
};
|
|
15
19
|
}
|
|
16
20
|
declare const CONFIG_FILE = "components.json";
|
|
17
21
|
declare function findConfigPath(cwd: string): string | null;
|
|
@@ -50,6 +54,8 @@ interface ProjectInfo {
|
|
|
50
54
|
framework: "next" | "vite" | "other";
|
|
51
55
|
rsc: boolean;
|
|
52
56
|
existingCssFile: string | null;
|
|
57
|
+
/** Path to the root layout file (layout.tsx for Next.js, index.html for Vite) */
|
|
58
|
+
layoutFile: string | null;
|
|
53
59
|
}
|
|
54
60
|
/**
|
|
55
61
|
* Detect the project structure to inform configuration defaults.
|
package/dist/utils/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stera-ui",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Add Stera UI components to your project.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@antfu/ni": "^30.0.0",
|
|
37
|
+
"@inquirer/prompts": "^7.0.0",
|
|
37
38
|
"commander": "^13.1.0",
|
|
38
39
|
"execa": "^9.6.1",
|
|
39
40
|
"tsconfig-paths": "^4.2.0",
|