@m1kapp/kit 0.0.32 → 0.0.33

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
@@ -458,14 +458,16 @@ npx m1kkit track https://myside.app
458
458
  ```
459
459
 
460
460
  ```bash
461
- # 2) .env 에 한 줄 → 이후 자동 집계 (스니펫 붙여넣기 불필요)
461
+ # 2) Next.js면 .env 에 한 줄 → 이후 자동 집계 (스니펫 붙여넣기 불필요)
462
462
  NEXT_PUBLIC_M1K_SLUG=your-slug
463
463
  ```
464
464
 
465
+ Vite 등 Next.js가 아닌 앱은 `NEXT_PUBLIC_*` env가 안 읽히니 `trackSlug` prop으로 직접 지정하세요:
466
+
465
467
  ```tsx
466
468
  // 끄기 / 명시 지정
467
469
  <Watermark track={false}>…</Watermark> // 비콘 끔
468
- <Watermark trackSlug="your-slug">…</Watermark> // env 대신 직접 지정
470
+ <Watermark trackSlug="your-slug">…</Watermark> // env 대신 직접 지정 (Vite는 이 방식 사용)
469
471
  // PoweredByKit 단독 사용 시: <PoweredByKit slug="your-slug" track={false} />
470
472
  ```
471
473
 
@@ -525,6 +527,20 @@ export async function GET() {
525
527
  <OGImage type="product" title="@m1kapp/kit" tagline="올인원 킷" features={["기능1", "기능2"]} color={c} bg={bg} />
526
528
  ```
527
529
 
530
+ ### CLI로 한 번에 (Next.js 라우트 없이)
531
+
532
+ Vite 등 서버 없는 SPA라 `app/og/route.tsx`를 못 만든다면, 빌드 없이 CLI로
533
+ 파비콘부터 OG 이미지, manifest까지 한 번에 생성하세요:
534
+
535
+ ```bash
536
+ npx m1kkit favicon --appname="My App" --slogan="한 줄 소개" --color=#3b82f6 --domain=myapp.com
537
+ ```
538
+
539
+ `favicon.ico` · `apple-touch-icon.png` · `icon-192.png` · `icon-512.png` ·
540
+ `icon-maskable-512.png` · `og-image.png`(1200×630, 위 `default` 템플릿 사용) ·
541
+ `manifest.json` 7개가 `public/`(자동 감지)에 한 번에 생기고, `<head>`에 붙여넣을
542
+ 태그 스니펫도 콘솔에 출력됩니다.
543
+
528
544
  ### 폰트 & 이모지
529
545
 
530
546
  ```tsx
package/bin/favicon.mjs CHANGED
@@ -1,16 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * @m1kapp/kit favicon generator CLI
3
+ * @m1kapp/kit favicon + SEO 이미지 생성 CLI
4
4
  *
5
5
  * Usage:
6
6
  * npx @m1kapp/kit favicon
7
7
  * npx @m1kapp/kit favicon --text=kit --color=#0f0f1a
8
8
  * npx @m1kapp/kit favicon --text=W --color=#7c3aed --out=./static
9
+ * npx @m1kapp/kit favicon --appname="My App" --slogan="한 줄 소개" --domain=myapp.com
9
10
  *
10
11
  * Options:
11
- * --text 표시할 텍스트 (기본: package.json name 첫 글자)
12
- * --color 배경색 hex (기본: #0f0f1a)
12
+ * --text 파비콘에 표시할 짧은 텍스트 (기본: package.json name 첫 글자)
13
+ * --appname OG 이미지/manifest용 앱 이름 (기본: package.json name)
14
+ * --slogan OG 이미지 부제
15
+ * --domain OG 이미지 하단 도메인 (선택)
16
+ * --color 브랜드색 hex (기본: #0f0f1a)
13
17
  * --out 출력 디렉토리 (기본: 자동 감지)
18
+ *
19
+ * 생성 파일: favicon.ico · apple-touch-icon.png · icon-192.png · icon-512.png ·
20
+ * icon-maskable-512.png · og-image.png · manifest.json
14
21
  */
15
22
 
16
23
  import { ImageResponse } from "@vercel/og";
@@ -61,48 +68,90 @@ function detectOutDir() {
61
68
  return cwd;
62
69
  }
63
70
 
64
- /* ─── Auto-detect text ─── */
65
- function detectText() {
66
- if (args.text) return args.text;
71
+ function readPkgName() {
67
72
  try {
68
73
  const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), "package.json"), "utf8"));
69
- const name = (pkg.name ?? "app").replace(/^@[^/]+\//, ""); // scoped 패키지 처리
70
- return name.slice(0, 3); // 최대 3글자
74
+ return (pkg.name ?? "app").replace(/^@[^/]+\//, ""); // scoped 패키지 처리
71
75
  } catch {
72
76
  return "app";
73
77
  }
74
78
  }
75
79
 
76
- /* ─── Load createFaviconElement from dist ─── */
80
+ /* ─── Auto-detect short favicon text ─── */
81
+ function detectText() {
82
+ if (args.text) return args.text;
83
+ return readPkgName().slice(0, 3); // 최대 3글자
84
+ }
85
+
86
+ /* ─── Auto-detect full app name (OG/manifest용) ─── */
87
+ function detectAppName() {
88
+ if (args.appname) return args.appname;
89
+ return readPkgName();
90
+ }
91
+
92
+ /* ─── Load generators from dist ─── */
77
93
  const distPath = path.resolve(__dirname, "../dist/ogimage.mjs");
78
- const { createFaviconElement } = await import(distPath);
94
+ const { createFaviconElement, OGImage } = await import(distPath);
79
95
 
80
96
  /* ─── Generate ─── */
81
97
  const text = detectText();
98
+ const appName = detectAppName();
99
+ const slogan = args.slogan;
100
+ const domain = args.domain;
82
101
  const color = args.color ?? "#0f0f1a";
83
102
  const outDir = detectOutDir();
84
103
 
85
- async function generatePng(sizePx) {
104
+ async function generatePng(sizePx, maskable = false) {
86
105
  const res = new ImageResponse(
87
- h(createFaviconElement, { text, color, size: sizePx }),
106
+ h(createFaviconElement, { text, color, size: sizePx, maskable }),
88
107
  { width: sizePx, height: sizePx }
89
108
  );
90
109
  return Buffer.from(await res.arrayBuffer());
91
110
  }
92
111
 
93
- fs.mkdirSync(outDir, { recursive: true });
112
+ function write(name, buffer) {
113
+ fs.writeFileSync(path.join(outDir, name), buffer);
114
+ console.log(`✓ ${path.relative(process.cwd(), path.join(outDir, name))}`);
115
+ }
94
116
 
95
- const icoBuffers = await Promise.all([16, 32, 48].map(generatePng));
96
- const icoBuffer = await pngToIco(icoBuffers);
97
- fs.writeFileSync(path.join(outDir, "favicon.ico"), icoBuffer);
98
- console.log(`✓ ${path.relative(process.cwd(), path.join(outDir, "favicon.ico"))}`);
117
+ fs.mkdirSync(outDir, { recursive: true });
99
118
 
100
- const appleBuffer = await generatePng(180);
101
- fs.writeFileSync(path.join(outDir, "apple-touch-icon.png"), appleBuffer);
102
- console.log(`✓ ${path.relative(process.cwd(), path.join(outDir, "apple-touch-icon.png"))}`);
119
+ const icoBuffers = await Promise.all([16, 32, 48].map((s) => generatePng(s)));
120
+ write("favicon.ico", await pngToIco(icoBuffers));
103
121
 
104
- const pwaBuffer = await generatePng(512);
105
- fs.writeFileSync(path.join(outDir, "icon-512.png"), pwaBuffer);
106
- console.log(`✓ ${path.relative(process.cwd(), path.join(outDir, "icon-512.png"))}`);
122
+ write("apple-touch-icon.png", await generatePng(180));
123
+ write("icon-192.png", await generatePng(192));
124
+ write("icon-512.png", await generatePng(512));
125
+ write("icon-maskable-512.png", await generatePng(512, true));
107
126
 
108
- console.log(`\n🎉 text="${text}" color="${color}" → ${path.relative(process.cwd(), outDir) || "."}/`);
127
+ const ogRes = new ImageResponse(
128
+ h(OGImage, { type: "default", title: appName, sub: slogan, appName, color, domain }),
129
+ { width: 1200, height: 630 }
130
+ );
131
+ write("og-image.png", Buffer.from(await ogRes.arrayBuffer()));
132
+
133
+ const manifest = {
134
+ name: appName,
135
+ short_name: appName,
136
+ start_url: "/",
137
+ display: "standalone",
138
+ background_color: color,
139
+ theme_color: color,
140
+ icons: [
141
+ { src: "/icon-192.png", sizes: "192x192", type: "image/png" },
142
+ { src: "/icon-512.png", sizes: "512x512", type: "image/png" },
143
+ { src: "/icon-maskable-512.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
144
+ ],
145
+ };
146
+ write("manifest.json", JSON.stringify(manifest, null, 2) + "\n");
147
+
148
+ console.log(`\n🎉 appname="${appName}" color="${color}" → ${path.relative(process.cwd(), outDir) || "."}/`);
149
+ console.log(`
150
+ <head>에 붙여넣기:
151
+ <link rel="icon" type="image/x-icon" href="/favicon.ico" />
152
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
153
+ <link rel="manifest" href="/manifest.json" />
154
+ <meta property="og:image" content="https://<도메인>/og-image.png" />
155
+ <meta property="og:image:width" content="1200" />
156
+ <meta property="og:image:height" content="630" />
157
+ `);
@@ -36,12 +36,19 @@ PWA 설정 현황
36
36
 
37
37
  빠진 항목에 대해 확인 후 적용한다.
38
38
 
39
- 아이콘이 없으면: `npx m1kkit favicon` 명령어 안내
40
- manifest가 없으면: 앱 이름, 테마 컬러를 물어보고 `createManifest` 적용
39
+ 아이콘이 없으면: `npx m1kkit favicon --appname="[앱 이름]" --color="[테마 컬러]"` 안내
40
+ — favicon.ico/apple-touch-icon/icon-192/icon-512/icon-maskable-512 뿐 아니라
41
+ og-image.png와 manifest.json까지 한 번에 만들어줌.
42
+
43
+ manifest가 없으면:
44
+ - Next.js면 앱 이름·테마 컬러를 물어보고 `createManifest` 적용 (아래).
45
+ - Vite 등 App Router가 없는 프로젝트면 `npx m1kkit favicon`이 만든
46
+ `public/manifest.json`을 그대로 쓰면 됨 — 따로 만들 필요 없음.
47
+
41
48
  viewport가 없으면: `mobileViewport` 적용
42
49
 
43
50
  ```ts
44
- // app/manifest.ts
51
+ // app/manifest.ts (Next.js App Router)
45
52
  import { createManifest } from "@m1kapp/kit/pwa"
46
53
 
47
54
  export default function manifest() {
package/dist/index.d.mts CHANGED
@@ -126,6 +126,13 @@ interface TabBarProps {
126
126
  }
127
127
  /**
128
128
  * Sticky bottom navigation tab bar.
129
+ *
130
+ * Carries no safe-area padding of its own — TabBar is meant to sit inside
131
+ * `AppShell`, which sits inside `Watermark`. `Watermark` appends its
132
+ * `PoweredByKit` credit strip right after `AppShell`, and that strip is the
133
+ * one that owns the home-indicator safe area (`variant="overlay"`). If
134
+ * TabBar also padded itself for the safe area, the reservation would be
135
+ * doubled — a blank gap between the tab icons and the credit strip.
129
136
  */
130
137
  declare function TabBar({ children, className }: TabBarProps): react_jsx_runtime.JSX.Element;
131
138
  interface TabRootProps {
package/dist/index.d.ts CHANGED
@@ -126,6 +126,13 @@ interface TabBarProps {
126
126
  }
127
127
  /**
128
128
  * Sticky bottom navigation tab bar.
129
+ *
130
+ * Carries no safe-area padding of its own — TabBar is meant to sit inside
131
+ * `AppShell`, which sits inside `Watermark`. `Watermark` appends its
132
+ * `PoweredByKit` credit strip right after `AppShell`, and that strip is the
133
+ * one that owns the home-indicator safe area (`variant="overlay"`). If
134
+ * TabBar also padded itself for the safe area, the reservation would be
135
+ * doubled — a blank gap between the tab icons and the credit strip.
129
136
  */
130
137
  declare function TabBar({ children, className }: TabBarProps): react_jsx_runtime.JSX.Element;
131
138
  interface TabRootProps {