@m1kapp/kit 0.0.25 → 0.0.27
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 +36 -0
- package/bin/skills/m1kapp-stats.md +70 -0
- package/bin/skills.mjs +4 -0
- package/bin/stats.mjs +4 -5
- package/dist/index.d.mts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +4 -4
- package/dist/index.mjs +3 -3
- package/dist/meta.json +338 -134
- package/dist/pwa.js +2 -2
- package/dist/pwa.mjs +1 -1
- package/dist/server.d.mts +118 -1
- package/dist/server.d.ts +118 -1
- package/dist/server.js +1 -1
- package/dist/server.mjs +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -750,6 +750,42 @@ export const GET = handler(async () => {
|
|
|
750
750
|
});
|
|
751
751
|
```
|
|
752
752
|
|
|
753
|
+
### 서버 유틸 (의존성 0)
|
|
754
|
+
|
|
755
|
+
API 라우트에서 자주 손으로 만들던 것들 — 전부 `@m1kapp/kit/server`에 있어요.
|
|
756
|
+
|
|
757
|
+
```ts
|
|
758
|
+
import {
|
|
759
|
+
requireEnv, fetchWithRetry, withRetry, recoverJsonFromText,
|
|
760
|
+
scrapeOg, todayKST, dateInTz, idToSlug, slugToId, appHost,
|
|
761
|
+
} from "@m1kapp/kit/server";
|
|
762
|
+
|
|
763
|
+
// 필수 env 검증 (없으면 500 throw, 있으면 타입된 객체)
|
|
764
|
+
const { XAI_API_KEY } = requireEnv(["XAI_API_KEY"]);
|
|
765
|
+
|
|
766
|
+
// fetch + 타임아웃 + 429/5xx 자동 재시도 (마지막 응답 반환)
|
|
767
|
+
const res = await fetchWithRetry(url, { headers: { authorization: `Bearer ${XAI_API_KEY}` } });
|
|
768
|
+
|
|
769
|
+
// 아무 async나 재시도 (Neon 콜드스타트 등)
|
|
770
|
+
const rows = await withRetry(() => db.query.users.findMany(), {
|
|
771
|
+
shouldRetry: (e) => String(e).includes("fetch failed"),
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
// LLM 응답에서 JSON 복구 (```json 펜스·트레일링 콤마·노이즈 제거)
|
|
775
|
+
const data = recoverJsonFromText<{ items: string[] }>(llmReply);
|
|
776
|
+
|
|
777
|
+
// Open Graph 스크래핑
|
|
778
|
+
const og = await scrapeOg("example.com"); // { title, description, image, siteName, url }
|
|
779
|
+
|
|
780
|
+
// 타임존 날짜
|
|
781
|
+
todayKST(); // "2026-06-04"
|
|
782
|
+
dateInTz(Date.now(), "America/New_York");
|
|
783
|
+
|
|
784
|
+
// base62 slug + 호스트
|
|
785
|
+
idToSlug(42, 1000); // "..." (slugToId로 역변환)
|
|
786
|
+
const base = `https://${appHost("m1k.app")}`;
|
|
787
|
+
```
|
|
788
|
+
|
|
753
789
|
---
|
|
754
790
|
|
|
755
791
|
## Utils
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
현재 프로젝트가 @m1kapp/kit을 쓰는 Next.js(또는 Vite/Remix) 앱인지 확인한 뒤, **코드 분석(stats)** 을 켜고 PoweredByKit 패널에 실제 데이터가 뜨게 만든다.
|
|
2
|
+
|
|
3
|
+
`m1kkit stats`는 소스를 스캔해 "kit이 대신 써준 코드량 / 컴포넌트·훅·유틸 사용률"을 계산하고 `public/kit-stats.json`을 만든다. 이 파일이 있어야 PoweredByKit 시트가 "N줄 필요했을 걸 / X%를 kit이 처리"를 보여준다. 없으면 패널은 "명령어를 실행하세요" 빈 상태만 뜬다.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Step 0: 파악 (사용자에게 보고하지 않음)
|
|
8
|
+
|
|
9
|
+
- `package.json` — @m1kapp/kit 설치 여부, `scripts.build`/`scripts.dev`, 이미 `m1kkit stats`가 박혀 있는지
|
|
10
|
+
- 소스 디렉토리 — App Router는 보통 `app/`이 루트(`src/` 없음). `src/`가 있으면 `--dir=src`, 없으면 `--dir=.`
|
|
11
|
+
- 빌드 산출물 경로 — Next는 `public/`이 정적 루트 → `--out=public`. Vite는 `public/`, 정적 호스팅은 배포 루트
|
|
12
|
+
- `public/kit-stats.json` 존재 여부 (있으면 이미 1회 생성됨 → 갱신만 하면 됨)
|
|
13
|
+
- `PoweredByKit`가 화면에 실제로 렌더되는지 (보통 `Watermark` 푸터 영역)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Step 1: stats 1회 실행
|
|
18
|
+
|
|
19
|
+
올바른 `--dir`/`--out`으로 한 번 돌려 데이터가 생기는지 확인한다.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx m1kkit stats --dir=. --out=public # App Router(루트가 app/)
|
|
23
|
+
# 또는
|
|
24
|
+
npx m1kkit stats --dir=src --out=public # src/ 구조
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- 성공하면 `public/kit-stats.json`이 생기고 `컴포넌트 N/30 · 절약 X줄` 요약이 출력된다.
|
|
28
|
+
- "디렉토리를 못 찾음" 류 에러 → `--dir`을 실제 소스 폴더로 교정해 재시도(App Router에서 `src`를 찾으면 안 됨).
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Step 2: 자동 갱신 — build/dev에 끼우기
|
|
33
|
+
|
|
34
|
+
매번 손으로 돌리지 않도록 `package.json` scripts에 합친다. **앞에 `|| true`를 붙여** stats 실패가 빌드를 막지 않게 한다.
|
|
35
|
+
|
|
36
|
+
```jsonc
|
|
37
|
+
{
|
|
38
|
+
"scripts": {
|
|
39
|
+
"stats": "m1kkit stats --dir=. --out=public",
|
|
40
|
+
"build": "m1kkit stats --dir=. --out=public || true && next build",
|
|
41
|
+
"dev": "m1kkit stats --dir=. --out=public || true && next dev"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- `next` 부분은 vite/remix 등 본인 프레임워크 명령으로 바꾼다.
|
|
47
|
+
- `--dir`/`--out`은 Step 1에서 통과한 값을 그대로 쓴다.
|
|
48
|
+
- 이미 `build`에 다른 prestep이 있으면 덮지 말고 앞에 체이닝만 추가.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Step 3: 패널에서 확인
|
|
53
|
+
|
|
54
|
+
- `PoweredByKit`(보통 `Watermark`가 자동 포함)을 열어 데이터가 뜨는지 본다.
|
|
55
|
+
- **데이터가 이미 있으면** 패널 하단에 `갱신하려면 npx m1kkit stats` 안내가 보인다 — 빈 상태(명령어 실행 안내)는 더 이상 안 나온다.
|
|
56
|
+
- 컴포넌트 사용률이 낮으면(예: 7/30) 더 쓸 만한 kit 컴포넌트를 권하고, 채택 후 stats를 **다시 돌려** 수치를 갱신한다.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Step 4: 커밋
|
|
61
|
+
|
|
62
|
+
- `public/kit-stats.json`은 **커밋한다**(배포 환경에서 패널이 즉시 뜨도록). 빌드에서 자동 생성되더라도, 정적 호스팅·프리뷰에서 누락되면 빈 상태가 보이므로 체크인해 두는 편이 안전하다.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 주의
|
|
67
|
+
|
|
68
|
+
- stats는 **소스 라인 수 기반 추정**이다. 정확한 회계가 아니라 "kit이 이만큼 덜 짜게 해줬다"는 감을 주는 용도.
|
|
69
|
+
- `--dir`을 잘못 잡으면(빈 폴더) 절약량이 0으로 나온다 — 항상 실제 소스 폴더를 가리킬 것.
|
|
70
|
+
- `public/`이 아닌 곳에 내보내면 런타임에서 `/kit-stats.json`을 못 불러온다(404 → 빈 상태). 정적 루트로 내보낼 것.
|
package/bin/skills.mjs
CHANGED
|
@@ -28,6 +28,10 @@ const SKILLS = [
|
|
|
28
28
|
name: "m1kapp-pwa",
|
|
29
29
|
desc: "/m1kapp-pwa — PWA 설정 점검 및 @m1kapp/kit/pwa 자동 적용",
|
|
30
30
|
},
|
|
31
|
+
{
|
|
32
|
+
name: "m1kapp-stats",
|
|
33
|
+
desc: "/m1kapp-stats — 코드 분석(stats) 켜기 · kit-stats.json 생성/갱신 · PoweredByKit 패널 데이터 채우기",
|
|
34
|
+
},
|
|
31
35
|
];
|
|
32
36
|
|
|
33
37
|
const args = process.argv.slice(2);
|
package/bin/stats.mjs
CHANGED
|
@@ -155,11 +155,10 @@ for (const name of allImports) {
|
|
|
155
155
|
// 카테고리별 사용 수 (loc 0이어도 카운트 — "Tab"도 사용한 거니까)
|
|
156
156
|
usedByCategory[meta.category] = (usedByCategory[meta.category] || 0) + 1;
|
|
157
157
|
|
|
158
|
-
// LOC 절약은 소스 파일 단위로 1
|
|
159
|
-
|
|
160
|
-
if (meta.source
|
|
161
|
-
|
|
162
|
-
if (meta.loc > 0) {
|
|
158
|
+
// LOC 절약은 소스 파일 단위로 1번만. 대표(loc>0)만 카운트하므로 import
|
|
159
|
+
// 순서와 무관 — 비대표(loc 0)가 먼저 와도 source를 선점하지 않는다.
|
|
160
|
+
if (meta.loc > 0 && !(meta.source && countedSources.has(meta.source))) {
|
|
161
|
+
if (meta.source) countedSources.add(meta.source);
|
|
163
162
|
usedFeatures.push({ name, loc: meta.loc, category: meta.category });
|
|
164
163
|
savedLines += meta.loc;
|
|
165
164
|
}
|
package/dist/index.d.mts
CHANGED
|
@@ -49,15 +49,24 @@ interface AppShellProps {
|
|
|
49
49
|
* Accent color (any CSS color) propagated to kit components as `--kit-accent`.
|
|
50
50
|
* Lets you re-skin the whole shell — Switch, SegmentedControl, ChatBubble,
|
|
51
51
|
* ActionCard, ListRow… — in one place. e.g. accent="#e2603f"
|
|
52
|
+
*
|
|
53
|
+
* When `accent` is a hex color, a contrasting foreground (`--kit-accent-fg`,
|
|
54
|
+
* black/white) is derived automatically so labels on the accent stay legible.
|
|
52
55
|
*/
|
|
53
56
|
accent?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Foreground color used *on top of* the accent (`--kit-accent-fg`). Defaults
|
|
59
|
+
* to an auto-derived black/white for hex accents, else white. Set this when
|
|
60
|
+
* `accent` is a non-hex CSS color (named/rgb/hsl) and the default contrasts poorly.
|
|
61
|
+
*/
|
|
62
|
+
accentFg?: string;
|
|
54
63
|
style?: CSSProperties;
|
|
55
64
|
}
|
|
56
65
|
/**
|
|
57
66
|
* Mobile app-like container with rounded corners, shadow, and ring.
|
|
58
67
|
* Centers content and constrains width for a phone-like viewport.
|
|
59
68
|
*/
|
|
60
|
-
declare function AppShell({ children, className, maxWidth, maxHeight, accent, style, }: AppShellProps): react_jsx_runtime.JSX.Element;
|
|
69
|
+
declare function AppShell({ children, className, maxWidth, maxHeight, accent, accentFg, style, }: AppShellProps): react_jsx_runtime.JSX.Element;
|
|
61
70
|
interface AppShellHeaderProps {
|
|
62
71
|
children: ReactNode;
|
|
63
72
|
className?: string;
|
|
@@ -114,6 +123,12 @@ interface TabProps {
|
|
|
114
123
|
}
|
|
115
124
|
/**
|
|
116
125
|
* Individual tab button for the TabBar.
|
|
126
|
+
*
|
|
127
|
+
* The active state is conveyed two ways so it works for BOTH `currentColor`
|
|
128
|
+
* SVG icons and full-color emoji icons (emoji ignore `color`): the inactive
|
|
129
|
+
* tab is dimmed (opacity + slight desaturation) with a muted label, while the
|
|
130
|
+
* active tab is full-strength with its label tinted `activeColor`. The icon is
|
|
131
|
+
* wrapped at a consistent size so mixed emoji/SVG icons line up.
|
|
117
132
|
*/
|
|
118
133
|
declare function Tab({ active, onClick, icon, label, activeColor }: TabProps): react_jsx_runtime.JSX.Element;
|
|
119
134
|
|
package/dist/index.d.ts
CHANGED
|
@@ -49,15 +49,24 @@ interface AppShellProps {
|
|
|
49
49
|
* Accent color (any CSS color) propagated to kit components as `--kit-accent`.
|
|
50
50
|
* Lets you re-skin the whole shell — Switch, SegmentedControl, ChatBubble,
|
|
51
51
|
* ActionCard, ListRow… — in one place. e.g. accent="#e2603f"
|
|
52
|
+
*
|
|
53
|
+
* When `accent` is a hex color, a contrasting foreground (`--kit-accent-fg`,
|
|
54
|
+
* black/white) is derived automatically so labels on the accent stay legible.
|
|
52
55
|
*/
|
|
53
56
|
accent?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Foreground color used *on top of* the accent (`--kit-accent-fg`). Defaults
|
|
59
|
+
* to an auto-derived black/white for hex accents, else white. Set this when
|
|
60
|
+
* `accent` is a non-hex CSS color (named/rgb/hsl) and the default contrasts poorly.
|
|
61
|
+
*/
|
|
62
|
+
accentFg?: string;
|
|
54
63
|
style?: CSSProperties;
|
|
55
64
|
}
|
|
56
65
|
/**
|
|
57
66
|
* Mobile app-like container with rounded corners, shadow, and ring.
|
|
58
67
|
* Centers content and constrains width for a phone-like viewport.
|
|
59
68
|
*/
|
|
60
|
-
declare function AppShell({ children, className, maxWidth, maxHeight, accent, style, }: AppShellProps): react_jsx_runtime.JSX.Element;
|
|
69
|
+
declare function AppShell({ children, className, maxWidth, maxHeight, accent, accentFg, style, }: AppShellProps): react_jsx_runtime.JSX.Element;
|
|
61
70
|
interface AppShellHeaderProps {
|
|
62
71
|
children: ReactNode;
|
|
63
72
|
className?: string;
|
|
@@ -114,6 +123,12 @@ interface TabProps {
|
|
|
114
123
|
}
|
|
115
124
|
/**
|
|
116
125
|
* Individual tab button for the TabBar.
|
|
126
|
+
*
|
|
127
|
+
* The active state is conveyed two ways so it works for BOTH `currentColor`
|
|
128
|
+
* SVG icons and full-color emoji icons (emoji ignore `color`): the inactive
|
|
129
|
+
* tab is dimmed (opacity + slight desaturation) with a muted label, while the
|
|
130
|
+
* active tab is full-strength with its label tinted `activeColor`. The icon is
|
|
131
|
+
* wrapped at a consistent size so mixed emoji/SVG icons line up.
|
|
117
132
|
*/
|
|
118
133
|
declare function Tab({ active, onClick, icon, label, activeColor }: TabProps): react_jsx_runtime.JSX.Element;
|
|
119
134
|
|