@m1kapp/kit 0.0.33 → 0.0.37

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
@@ -22,6 +22,7 @@ import { Watermark, AppShell, AppShellHeader, AppShellContent, Section, TabBar,
22
22
  export default function App() {
23
23
  return (
24
24
  // Watermark가 뷰포트 전체 높이/중앙정렬/하단 크레딧을 같이 줍니다.
25
+ // (크레딧 스트립은 셸 바로 밑에 붙어서 셸과 한 덩어리로 중앙정렬됨 — 화면 진짜 바닥에 고정되는 게 아님)
25
26
  // AppShell만 단독으로 쓰면 콘텐츠 높이만큼 쪼그라들어요.
26
27
  <Watermark color="#e2603f" text="myapp">
27
28
  <AppShell>
@@ -5,9 +5,24 @@
5
5
  - 최악 함수 cognitive 20 초과분 ×1 (최대 -15)
6
6
  - **중복 밀도 3% 초과분 ×2.5 (최대 -25)** — 토큰 단위 복붙 검출 (식별자 정규화, 테스트 제외)
7
7
  - 200줄 초과 파일 비율 ×2 (최대 -10) / 평균 파일 길이 80줄 초과분 ÷5 (최대 -10)
8
+ - **루프 안 무캐시 파일읽기 ×4 (최대 -20)** — 호출 1번이 파일을 N번 읽는 구조 (`quality.io`)
9
+ - **렌더 인질 ×3 (최대 -10)** — fetch 하나가 무관한 UI까지 막는 자리 (`quality.renderGates`)
8
10
 
9
11
  cognitive = 중첩 깊이 가중 복잡도 (SonarQube 방식): `a && b && c`는 1, 5중 중첩 if는 flat if 5개보다 훨씬 비쌈. `??`는 미카운트. 즉 **평평하게 펴는 것(조기 반환)이 진짜 점수가 됨** — 함수 쪼개기만으론 안 오름.
10
12
 
13
+ `io` 축은 나머지와 성격이 다르다. cognitive·중복은 "코드가 얼마나 꼬였나"라 5줄짜리
14
+ 흠 없는 리더가 루프에서 120번 불리는 종류는 **원리상 못 잡는다** — 실제로 요청당 파일을
15
+ 492번 읽던 코드가 A+ 100점을 통과한 적이 있다. 그래서 파일을 읽는 함수(한 다리 건너
16
+ 읽는 얇은 래퍼 포함)를 찾아 루프·`map` 안에서 불리는 자리를 센다. 그 리더가 캐시를
17
+ 끼고 있으면(모듈 Map을 get/set) 반복 호출돼도 실제 읽기는 한 번이라 감점하지 않는다.
18
+ **고치는 법은 리팩토링이 아니라 캐시를 끼우거나 한 번 읽어 재사용하도록 구조를 바꾸는 것** —
19
+ `quality.io.worst`가 위치를 지목한다.
20
+
21
+ `renderGates`도 같은 성격이다. `{data && <Nav a={local} b={data.x} onChange={fn} />}`처럼
22
+ 프롭 3개 중 1개만 그 데이터에 의존하는데 전체가 대기하면, 사용자에겐 이미 아는 UI까지
23
+ 매번 로딩으로 보인다. 데이터에 의존하는 부분만 늦게 채우고 나머지는 먼저 그리는 게 답이다
24
+ (서버가 아는 값이면 아예 서버에서 내려주면 된다).
25
+
11
26
  즉 **효율 순서: ① 중복 블록 제거 (kit-stats.json `quality.duplication.worstBlocks`가 위치 지목) ② 최악 함수 중첩 평탄화 ③ cog15+ 함수들 ④ 긴 파일 분할**.
12
27
 
13
28
  `--llm` 플래그(선택): cognitive 최악 3개를 Claude(haiku)가 네이밍·응집도·본질적 복잡성 여부 자문 — 점수 미반영, `quality.llm`에 기록. "essential: true"로 나온 함수는 억지로 쪼개지 말 것.
package/bin/stats.mjs CHANGED
@@ -340,6 +340,306 @@ function analyzeDuplication(ts, fileContents) {
340
340
  return { percent, dupTokens, totalTokens, blocks: blockKeys.size, worstFiles, worstBlocks };
341
341
  }
342
342
 
343
+ // 파일 I/O 밀도 — "한 번 처리하는 데 파일을 몇 번이나 읽게 되는 구조인가".
344
+ // cognitive·중복은 코드 모양만 보므로, 5줄짜리 흠 없는 리더가 루프에서 120번 불리는
345
+ // 종류는 원리상 못 잡는다(그래서 요청당 492회 읽던 코드가 A+ 100점을 통과했다).
346
+ // 여기선 리더 함수를 찾고, 그게 루프 안에서 불리는 자리를 센다. 리더가 캐시를 끼고
347
+ // 있으면 반복 호출돼도 실제 읽기는 한 번이라 감점하지 않고 참고로만 남긴다.
348
+ const FILE_READ_CALLS = new Set(["readFileSync", "readFile", "readdirSync"]);
349
+ const ITERATING_METHODS = new Set(["map", "forEach", "flatMap", "filter", "reduce", "some", "every", "find"]);
350
+
351
+ function analyzeIoDensity(ts, fileContents) {
352
+ const parsed = fileContents.map(({ file, content }) => ({
353
+ file,
354
+ sf: ts.createSourceFile(
355
+ file,
356
+ content,
357
+ ts.ScriptTarget.Latest,
358
+ true,
359
+ /\.(tsx|jsx)$/.test(file) ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
360
+ ),
361
+ }));
362
+
363
+ const isFnLike = (n) =>
364
+ ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isArrowFunction(n) ||
365
+ ts.isMethodDeclaration(n);
366
+
367
+ const nameOf = (sf, n) => {
368
+ if (n.name) return n.name.getText(sf);
369
+ const p = n.parent;
370
+ if (p && ts.isVariableDeclaration(p)) return p.name.getText(sf);
371
+ return null;
372
+ };
373
+
374
+ const lineOf = (sf, n) => sf.getLineAndCharacterOfPosition(n.getStart(sf)).line + 1;
375
+
376
+ // 1단계: 함수마다 "직접 파일을 읽는가 / 자기 캐시를 쓰는가 / 누구를 부르는가"를 모은다
377
+ const fns = new Map(); // 이름 -> { file, line, readsDirectly, hasOwnCache, calls:Set }
378
+ for (const { file, sf } of parsed) {
379
+ const moduleHasMap = /new (Map|WeakMap)\s*[<(]/.test(sf.text);
380
+ // 모듈 스코프 변수 이름 — "읽고 또 대입하는" 변수를 쓰면 그게 메모이제이션이다
381
+ // (globalThis에 물린 캐시나 `let memo = null` 패턴. Map만 캐시로 보면 이런 걸 다 놓친다)
382
+ const moduleVars = new Set();
383
+ for (const stmt of sf.statements) {
384
+ if (ts.isVariableStatement(stmt)) {
385
+ for (const decl of stmt.declarationList.declarations) {
386
+ if (ts.isIdentifier(decl.name)) moduleVars.add(decl.name.getText(sf));
387
+ }
388
+ }
389
+ }
390
+
391
+ const collect = (node) => {
392
+ if (isFnLike(node) && node.body) {
393
+ let readsDirectly = false;
394
+ let mapCacheOps = 0;
395
+ const calls = new Set();
396
+ const readVars = new Set();
397
+ const writtenVars = new Set();
398
+ const rootName = (expr) => {
399
+ let cur = expr;
400
+ while (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur)) cur = cur.expression;
401
+ return ts.isIdentifier(cur) ? cur.getText(sf) : null;
402
+ };
403
+ const scan = (n) => {
404
+ if (n !== node && isFnLike(n)) return; // 중첩 함수는 자기 항목에서 본다
405
+ if (ts.isCallExpression(n)) {
406
+ const callee = n.expression;
407
+ const called = ts.isPropertyAccessExpression(callee) ? callee.name.getText(sf) : callee.getText(sf);
408
+ if (FILE_READ_CALLS.has(called)) readsDirectly = true;
409
+ if (called === "get" || called === "set") mapCacheOps++;
410
+ calls.add(called);
411
+ }
412
+ if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
413
+ const target = rootName(n.left);
414
+ if (target && moduleVars.has(target)) writtenVars.add(target);
415
+ }
416
+ if (ts.isIdentifier(n) && moduleVars.has(n.getText(sf))) readVars.add(n.getText(sf));
417
+ ts.forEachChild(n, scan);
418
+ };
419
+ ts.forEachChild(node, scan);
420
+ const memoizesByVar = [...writtenVars].some((v) => readVars.has(v));
421
+ const name = nameOf(sf, node);
422
+ if (name && !fns.has(name)) {
423
+ fns.set(name, {
424
+ file,
425
+ line: lineOf(sf, node),
426
+ readsDirectly,
427
+ // 캐시로 보는 두 형태: 모듈 Map을 get/set 하거나, 모듈 변수를 읽고 다시 대입하거나
428
+ hasOwnCache: (moduleHasMap && mapCacheOps >= 2) || memoizesByVar,
429
+ calls,
430
+ });
431
+ }
432
+ }
433
+ ts.forEachChild(node, collect);
434
+ };
435
+ ts.forEachChild(sf, collect);
436
+ }
437
+
438
+ // 2단계: 호출 그래프로 "리더"를 전파한다 — getThemeComments → readJson → readFileSync 처럼
439
+ // 직접 읽지 않고 한 다리 건너 읽는 함수가 실제 N+1의 주인공이라 이게 핵심이다.
440
+ // 읽기 경로가 전부 캐시를 거치면 그 함수도 캐시된 리더로 본다(반복 호출돼도 읽기는 한 번).
441
+ const readers = new Map(); // 이름 -> { file, line, cached }
442
+ for (const [name, fn] of fns) {
443
+ if (fn.readsDirectly) readers.set(name, { file: fn.file, line: fn.line, cached: fn.hasOwnCache });
444
+ }
445
+ const MAX_HOPS = 2; // 얇은 래퍼(getThemeComments → readJson)까지만. 더 깊으면 전부 리더가 된다
446
+ for (let pass = 0; pass < MAX_HOPS; pass++) {
447
+ let changed = false;
448
+ for (const [name, fn] of fns) {
449
+ if (readers.has(name)) continue;
450
+ const calledReaders = [...fn.calls].map((c) => readers.get(c)).filter(Boolean);
451
+ if (calledReaders.length === 0) continue;
452
+ readers.set(name, {
453
+ file: fn.file,
454
+ line: fn.line,
455
+ cached: fn.hasOwnCache || calledReaders.every((r) => r.cached),
456
+ });
457
+ changed = true;
458
+ }
459
+ if (!changed) break;
460
+ }
461
+
462
+ // 3단계: 그 리더(또는 fs 읽기 자체)가 루프 안에서 불리는 자리를 찾는다.
463
+ // for/while뿐 아니라 map·forEach 같은 순회 콜백 안도 루프로 본다.
464
+ const sites = [];
465
+ for (const { file, sf } of parsed) {
466
+ // 캐시를 직접 관리하는 함수 안에서의 읽기는 캐시 미스일 때만 나간다(2단 캐시의 디스크 티어 등)
467
+ const walk = (node, inLoop, inCachingFn) => {
468
+ let childInLoop = inLoop;
469
+ let childInCachingFn = inCachingFn;
470
+ if (isFnLike(node)) {
471
+ const self = fns.get(nameOf(sf, node));
472
+ if (self && self.hasOwnCache) childInCachingFn = true;
473
+ }
474
+ switch (node.kind) {
475
+ case ts.SyntaxKind.ForStatement:
476
+ case ts.SyntaxKind.ForInStatement:
477
+ case ts.SyntaxKind.ForOfStatement:
478
+ case ts.SyntaxKind.WhileStatement:
479
+ case ts.SyntaxKind.DoStatement:
480
+ childInLoop = true;
481
+ break;
482
+ }
483
+ if (ts.isCallExpression(node)) {
484
+ const callee = node.expression;
485
+ const called = ts.isPropertyAccessExpression(callee) ? callee.name.getText(sf) : callee.getText(sf);
486
+ const reader = readers.get(called);
487
+ if (inLoop && (FILE_READ_CALLS.has(called) || reader)) {
488
+ const cached = inCachingFn || (reader ? reader.cached : false);
489
+ sites.push({ file, line: lineOf(sf, node), callee: called, cached });
490
+ }
491
+ // 순회 메서드에 넘긴 콜백 본문은 루프 안으로 취급
492
+ if (ts.isPropertyAccessExpression(callee) && ITERATING_METHODS.has(callee.name.getText(sf))) {
493
+ for (const arg of node.arguments) {
494
+ if (isFnLike(arg)) walk(arg, true, childInCachingFn);
495
+ }
496
+ }
497
+ }
498
+ ts.forEachChild(node, (child) => {
499
+ const alreadyWalked =
500
+ ts.isCallExpression(node) &&
501
+ ts.isPropertyAccessExpression(node.expression) &&
502
+ ITERATING_METHODS.has(node.expression.name.getText(sf)) &&
503
+ isFnLike(child);
504
+ if (!alreadyWalked) walk(child, childInLoop, childInCachingFn);
505
+ });
506
+ };
507
+ ts.forEachChild(sf, (n) => walk(n, false, false));
508
+ }
509
+
510
+ const uncached = sites.filter((s) => !s.cached);
511
+ const readerList = [...readers.entries()].map(([name, r]) => ({ name, ...r }));
512
+ return {
513
+ readers: readerList.length,
514
+ uncachedReaders: readerList.filter((r) => !r.cached).length,
515
+ loopSites: sites.length,
516
+ uncachedLoopSites: uncached.length,
517
+ worst: uncached.slice(0, 5).map(({ file, line, callee }) => ({ file, line, callee })),
518
+ };
519
+ }
520
+
521
+ // 렌더 인질 — 데이터 하나를 기다리느라 그와 무관한 UI까지 통째로 못 그리는 자리.
522
+ // `{data && <Nav a={local} b={data.x} onChange={fn} />}` 처럼 프롭 대부분이 이미 아는
523
+ // 값인데 fetch 하나 때문에 전체가 대기하면, 사용자에겐 매번 로딩으로 보인다.
524
+ // 게이트에 걸린 요소가 그 데이터에 실제로 의존하는 비율로 판정한다.
525
+ const HOSTAGE_DEP_RATIO = 0.5; // 프롭 절반도 안 쓰면서 전체를 막고 있으면 인질
526
+
527
+ function analyzeRenderGates(ts, fileContents) {
528
+ const findings = [];
529
+
530
+ for (const { file, content } of fileContents) {
531
+ if (!/\.(tsx|jsx)$/.test(file)) continue;
532
+ const sf = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
533
+
534
+ // 훅이 물어다 주는 값(로딩이 있는 값)을 찾는다: const { data: x } = useSomething()
535
+ const fetched = new Set();
536
+ const findHookVars = (n) => {
537
+ if (
538
+ ts.isVariableDeclaration(n) &&
539
+ n.initializer &&
540
+ ts.isCallExpression(n.initializer) &&
541
+ /^use[A-Z]/.test(
542
+ ts.isPropertyAccessExpression(n.initializer.expression)
543
+ ? n.initializer.expression.name.getText(sf)
544
+ : n.initializer.expression.getText(sf),
545
+ ) &&
546
+ ts.isObjectBindingPattern(n.name)
547
+ ) {
548
+ for (const el of n.name.elements) {
549
+ const source = (el.propertyName || el.name).getText(sf);
550
+ if (source === "data") fetched.add(el.name.getText(sf));
551
+ }
552
+ }
553
+ ts.forEachChild(n, findHookVars);
554
+ };
555
+ ts.forEachChild(sf, findHookVars);
556
+ if (fetched.size === 0) continue;
557
+
558
+ const rootName = (expr) => {
559
+ let cur = expr;
560
+ while (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur)) cur = cur.expression;
561
+ return ts.isIdentifier(cur) ? cur.getText(sf) : null;
562
+ };
563
+
564
+ /** 이 노드가 그 변수를 실제로 참조하는가 */
565
+ const references = (node, name) => {
566
+ let found = false;
567
+ const scan = (n) => {
568
+ if (found) return;
569
+ if (ts.isIdentifier(n) && n.getText(sf) === name) found = true;
570
+ else ts.forEachChild(n, scan);
571
+ };
572
+ scan(node);
573
+ return found;
574
+ };
575
+
576
+ /** 게이트에 쓰인 fetch 변수들 */
577
+ const gateVars = (expr) => {
578
+ const names = new Set();
579
+ const scan = (n) => {
580
+ if (ts.isIdentifier(n) && fetched.has(n.getText(sf))) names.add(n.getText(sf));
581
+ ts.forEachChild(n, scan);
582
+ };
583
+ scan(expr);
584
+ return [...names];
585
+ };
586
+
587
+ const inspectGate = (testExpr, element) => {
588
+ const vars = gateVars(testExpr);
589
+ if (vars.length === 0) return;
590
+ const el = ts.isJsxElement(element) ? element.openingElement : element;
591
+ if (!el.attributes) return;
592
+ const props = el.attributes.properties;
593
+ if (props.length < 2) return; // 프롭이 거의 없으면 판단할 근거가 없다
594
+
595
+ const dependent = props.filter((p) => vars.some((v) => references(p, v))).length;
596
+ const ratio = dependent / props.length;
597
+ if (ratio < HOSTAGE_DEP_RATIO) {
598
+ findings.push({
599
+ file,
600
+ line: sf.getLineAndCharacterOfPosition(el.getStart(sf)).line + 1,
601
+ element: el.tagName ? el.tagName.getText(sf) : "?",
602
+ gate: vars.join(", "),
603
+ dependentProps: dependent,
604
+ totalProps: props.length,
605
+ });
606
+ }
607
+ };
608
+
609
+ const walk = (n) => {
610
+ // {data && <El .../>}
611
+ if (
612
+ ts.isBinaryExpression(n) &&
613
+ n.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
614
+ (ts.isJsxElement(n.right) || ts.isJsxSelfClosingElement(n.right))
615
+ ) {
616
+ inspectGate(n.left, n.right);
617
+ }
618
+ // {data ? <El .../> : null}
619
+ if (ts.isConditionalExpression(n) && (ts.isJsxElement(n.whenTrue) || ts.isJsxSelfClosingElement(n.whenTrue))) {
620
+ inspectGate(n.condition, n.whenTrue);
621
+ }
622
+ // 괄호로 감싼 JSX도 같은 취급
623
+ if (
624
+ ts.isBinaryExpression(n) &&
625
+ n.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
626
+ ts.isParenthesizedExpression(n.right) &&
627
+ (ts.isJsxElement(n.right.expression) || ts.isJsxSelfClosingElement(n.right.expression))
628
+ ) {
629
+ inspectGate(n.left, n.right.expression);
630
+ }
631
+ ts.forEachChild(n, walk);
632
+ };
633
+ ts.forEachChild(sf, walk);
634
+ void rootName;
635
+ }
636
+
637
+ return {
638
+ hostages: findings.length,
639
+ worst: findings.slice(0, 5),
640
+ };
641
+ }
642
+
343
643
  // 파일 분류 — frontend(UI) / backend(API·서버) / shared(공용 유틸)
344
644
  function classifyFile(filePath, content) {
345
645
  const rel = filePath.replace(/\\/g, "/");
@@ -439,10 +739,15 @@ const avgFileLines = files.length > 0 ? Math.round(codeLines / files.length) : 0
439
739
  // ※ 파일 개수 기준 이진 카운트(200줄 넘으면 무조건 1) 대신 심각도 가중 — 살짝 넘는 파일과
440
740
  // 터무니없이 큰 파일을 구분하고, 파일 수 적은 프로젝트가 파일 1개만으로 즉시 만점 감점 맞는 절벽 방지.
441
741
  // / 평균 파일 길이 80줄 초과분 /5 (최대 10)
742
+ // - 렌더 인질(fetch 하나가 무관한 UI까지 막는 자리) ×3 (최대 10)
743
+ // - 루프 안 무캐시 파일읽기 사이트 ×4 (최대 20) ← 새 축. 코드 모양이 아무리 깔끔해도
744
+ // 호출 1번이 파일 N번을 읽는 구조는 여기서만 드러난다(cognitive·중복으론 원리상 안 잡힘).
442
745
  let qualityScore;
443
746
  let cc = null;
444
747
  let cognitive = null;
445
748
  let duplication = null;
749
+ let io = null;
750
+ let renderGates = null;
446
751
  if (ts && allFns.length > 0) {
447
752
  const byCc = [...allFns].sort((a, b) => b.cc - a.cc);
448
753
  cc = {
@@ -469,6 +774,8 @@ if (ts && allFns.length > 0) {
469
774
  };
470
775
 
471
776
  duplication = analyzeDuplication(ts, fileContents);
777
+ io = analyzeIoDensity(ts, fileContents);
778
+ renderGates = analyzeRenderGates(ts, fileContents);
472
779
 
473
780
  const over15Pct = (cogOver15.length / allFns.length) * 100;
474
781
  const over25Pct = (cogOver25.length / allFns.length) * 100;
@@ -481,6 +788,8 @@ if (ts && allFns.length > 0) {
481
788
  - Math.min(25, Math.max(0, duplication.percent - 3) * 2.5)
482
789
  - Math.min(10, longFileSeverityPct * 1.5)
483
790
  - Math.min(10, Math.max(0, avgFileLines - 80) / 5)
791
+ - Math.min(20, io.uncachedLoopSites * 4)
792
+ - Math.min(10, renderGates.hostages * 3)
484
793
  ));
485
794
  } else {
486
795
  // regex 폴백 (typescript 미설치)
@@ -543,6 +852,8 @@ const stats = {
543
852
  grade: qualityGrade,
544
853
  cognitive, // {avg, p90, max, over15, over25, worst[5]} — 중첩 가중 복잡도
545
854
  duplication, // {percent, blocks, worstFiles, worstBlocks} — 토큰 중복 밀도
855
+ io, // {readers, uncachedReaders, loopSites, uncachedLoopSites, worst} — 루프 안 파일 읽기
856
+ renderGates, // {hostages, worst} — fetch 하나가 무관한 UI까지 막고 있는 자리
546
857
  cc, // {functions, avg, p90, max, over10, over20, worst[5]} — McCabe (참고용)
547
858
  branchDensity, // 100줄당 분기 수 (regex 근사, 참고용)
548
859
  branches: totalBranches,
@@ -588,6 +899,18 @@ if (cc) {
588
899
  console.log(` 중복 블록: ${ex.join(" ≒ ")}`);
589
900
  }
590
901
  }
902
+ if (renderGates.hostages > 0) {
903
+ console.log(` 렌더 인질: ${renderGates.hostages}곳 — 데이터를 기다리느라 이미 아는 UI까지 못 그립니다`);
904
+ for (const w of renderGates.worst) {
905
+ console.log(` 인질: <${w.element}> ${w.gate} 대기 (의존 프롭 ${w.dependentProps}/${w.totalProps}) — ${w.file}:${w.line}`);
906
+ }
907
+ }
908
+ if (io.uncachedLoopSites > 0) {
909
+ console.log(` 루프 안 파일읽기: ${io.uncachedLoopSites}곳 (캐시 없는 리더 ${io.uncachedReaders}/${io.readers}개) — 호출 1번이 파일 N번 읽습니다`);
910
+ for (const w of io.worst) {
911
+ console.log(` 반복 읽기: ${w.callee}() — ${w.file}:${w.line}`);
912
+ }
913
+ }
591
914
  } else {
592
915
  console.log(` 청결도: ${qualityGrade} (${qualityScore}점) — 분기밀도 ${branchDensity}/100줄, 평균 ${avgFileLines}줄/파일, 200줄+ ${longFiles}개 (regex 폴백 — typescript 설치 시 AST 정밀 분석)`);
593
916
  }
package/dist/index.d.mts CHANGED
@@ -71,6 +71,10 @@ interface AppShellProps {
71
71
  * strip) rather than a hand-rolled height/centering div; without a sized
72
72
  * parent the shell just collapses to content height.
73
73
  *
74
+ * The shell and the credit strip are centered together as one group — the
75
+ * strip sits directly under the shell and moves with it, it does not pin to
76
+ * the screen's true bottom edge.
77
+ *
74
78
  * @example
75
79
  * <Watermark color="#e2603f" text="myapp">
76
80
  * <AppShell>
@@ -84,8 +88,21 @@ interface AppShellHeaderProps {
84
88
  children: ReactNode;
85
89
  className?: string;
86
90
  }
91
+ /**
92
+ * `AppShellHeader`'s rendered height (base 3.5rem/56px + the top safe-area
93
+ * inset on notched devices). Pass this as `<FetchProgress top={APP_SHELL_HEADER_HEIGHT}>`
94
+ * instead of hardcoding `56` so the progress bar still sits flush under the
95
+ * header on a device with a Dynamic Island / notch.
96
+ */
97
+ declare const APP_SHELL_HEADER_HEIGHT = "calc(3.5rem + env(safe-area-inset-top))";
87
98
  /**
88
99
  * Sticky top header with blur backdrop.
100
+ *
101
+ * On mobile the shell is full-bleed (no `sm:` margin from Watermark), so this
102
+ * is the actual topmost element under the device status bar / Dynamic Island
103
+ * in an installed PWA — `min-height` (not `height`) plus `padding-top` let the
104
+ * safe-area inset grow the header without squeezing its content, the same
105
+ * ownership split PoweredByKit uses for the bottom inset.
89
106
  */
90
107
  declare function AppShellHeader({ children, className }: AppShellHeaderProps): react_jsx_runtime.JSX.Element;
91
108
  interface AppShellContentProps {
@@ -102,6 +119,8 @@ declare function AppShellContent({ children, className }: AppShellContentProps):
102
119
  interface FabProps {
103
120
  onClick: () => void;
104
121
  icon: ReactNode;
122
+ /** Accessible label — icon-only button has no other text for screen readers */
123
+ label: string;
105
124
  /** Accent background color. Default: zinc-900/white (follows dark mode) */
106
125
  color?: string;
107
126
  className?: string;
@@ -115,10 +134,10 @@ interface FabProps {
115
134
  * @example
116
135
  * <AppShellContent>
117
136
  * {content}
118
- * <Fab onClick={handleAdd} icon={<PlusIcon />} color="#6366f1" />
137
+ * <Fab onClick={handleAdd} icon={<PlusIcon />} label="추가" color="#6366f1" />
119
138
  * </AppShellContent>
120
139
  */
121
- declare function Fab({ onClick, icon, color, className }: FabProps): react_jsx_runtime.JSX.Element;
140
+ declare function Fab({ onClick, icon, label, color, className }: FabProps): react_jsx_runtime.JSX.Element;
122
141
 
123
142
  interface TabBarProps {
124
143
  children: ReactNode;
@@ -183,8 +202,13 @@ interface FetchProgressProps {
183
202
  * When omitted, the bar shows while ANY useFetch request is in flight.
184
203
  */
185
204
  active?: boolean;
186
- /** Distance from the top of the positioned ancestor, in px (e.g. header height). Default: 0 */
187
- top?: number;
205
+ /**
206
+ * Distance from the top of the positioned ancestor — a number of px, or any
207
+ * CSS length (e.g. `"calc(3.5rem + env(safe-area-inset-top))"` to sit below
208
+ * `AppShellHeader`, whose height grows with the safe-area inset on notched
209
+ * devices). Default: 0
210
+ */
211
+ top?: number | string;
188
212
  /** Bar thickness in px. Default: 3 */
189
213
  height?: number;
190
214
  className?: string;
@@ -1768,4 +1792,4 @@ declare function parseIsoDuration(duration: string): IsoDuration;
1768
1792
  /** "PT1H2M3S" → 3723 (초) */
1769
1793
  declare function isoDurationToSec(duration: string): number;
1770
1794
 
1771
- export { ActionCard, type ActionCardLabels, type ActionCardProps, type ActionCardState, type ApiClient, type ApiClientOptions, ApiError, AppShell, AppShellContent, type AppShellContentProps, AppShellHeader, type AppShellHeaderProps, type AppShellProps, AsyncList, type AsyncListProps, type AsyncListStatus, Avatar, type AvatarProps, Badge, type BadgeProps, BarList, type BarListItem, type BarListProps, Button, type ButtonProps, Carousel, type CarouselProps, ChatBubble, type ChatBubbleProps, type ChatMessage, type ChatRole, CodeBlock, type CodeBlockProps, Collapsible, type CollapsibleProps, type ColorName, ColorPicker, type ColorPickerProps, CopyButton, type CopyButtonProps, Countdown, type CountdownLabels, type CountdownProps, type DateCount, type DayGroup, Dialog, type DialogProps, Divider, type DividerProps, EmojiButton, type EmojiButtonProps, EmojiPicker, type EmojiPickerLabels, type EmojiPickerProps, EmptyState, type EmptyStateProps, Fab, type FabProps, FetchProgress, type FetchProgressProps, Field, type FieldProps, FontLinks, type FontName, GrassMap, type GrassMapData, type GrassMapLabels, type GrassMapProps, IOSInstallSheet, IconButton, type IconButtonProps, Img, type ImgProps, InAppSheet, type InAppSheetProps, InlineEdit, type InlineEditProps, Input, type InputProps, type IsoDuration, KitStyles, LinkifiedText, type LinkifiedTextProps, ListRow, type ListRowProps, MediaCard, type MediaCardProps, MessageList, type MessageListProps, PWAInstallButton, type PWAInstallState, PoweredByKit, type PoweredByKitProps, ProgressRing, type ProgressRingProps, Section, SectionHeader, type SectionHeaderProps, type SectionProps, type SegmentOption, SegmentedControl, type SegmentedControlProps, Select, type SelectOption, type SelectProps, ShareButton, type ShareButtonProps, Skeleton, type SkeletonProps, StatChip, type StatChipProps, type Step, Stepper, type StepperProps, Switch, type SwitchProps, THEME_SCRIPT, Tab, TabBar, type TabBarProps, type TabProps, type TabRootProps, Textarea, type TextareaProps, ThemeButton, type ThemeButtonProps, ThemeDialog, type ThemeDialogLabels, type ThemeDialogProps, type ToastOptions, ToastProvider, type ToastVariant, Tooltip, type TooltipProps, Typewriter, type TypewriterProps, TypingIndicator, type TypingIndicatorProps, type UnderlineTab, UnderlineTabs, type UnderlineTabsProps, type UseFetchOptions, type UseFetchResult, type UseFormSubmitOptions, type UseFormSubmitResult, type UseInViewOptions, type UseInViewResult, type UsePWAInstallReturn, type UsePollingOptions, type UsePollingResult, type UseShareOptions, type UseShareReturn, Watermark, type WatermarkProps, type WatermarkSponsor, clearFetchCache, cn, colors, createApiClient, createManifest, fillDateSeries, fontFamily, fonts, formatDayLabel, formatDuration, formatKoreanNumber, formatNumber, formatPrice, getFetchActiveCount, groupByDay, invalidateFetch, isoDurationToSec, mobileViewport, parseIsoDuration, postJson, relativeTime, subscribeFetchActivity, svgIcon, useCopy, useDebounce, useEscapeKey, useFetch, useFocusTrap, useFormSubmit, useImageLoader, useInView, useLocalStorage, usePWAInstall, usePolling, usePortalTarget, useScrollLock, useShare, useToast };
1795
+ export { APP_SHELL_HEADER_HEIGHT, ActionCard, type ActionCardLabels, type ActionCardProps, type ActionCardState, type ApiClient, type ApiClientOptions, ApiError, AppShell, AppShellContent, type AppShellContentProps, AppShellHeader, type AppShellHeaderProps, type AppShellProps, AsyncList, type AsyncListProps, type AsyncListStatus, Avatar, type AvatarProps, Badge, type BadgeProps, BarList, type BarListItem, type BarListProps, Button, type ButtonProps, Carousel, type CarouselProps, ChatBubble, type ChatBubbleProps, type ChatMessage, type ChatRole, CodeBlock, type CodeBlockProps, Collapsible, type CollapsibleProps, type ColorName, ColorPicker, type ColorPickerProps, CopyButton, type CopyButtonProps, Countdown, type CountdownLabels, type CountdownProps, type DateCount, type DayGroup, Dialog, type DialogProps, Divider, type DividerProps, EmojiButton, type EmojiButtonProps, EmojiPicker, type EmojiPickerLabels, type EmojiPickerProps, EmptyState, type EmptyStateProps, Fab, type FabProps, FetchProgress, type FetchProgressProps, Field, type FieldProps, FontLinks, type FontName, GrassMap, type GrassMapData, type GrassMapLabels, type GrassMapProps, IOSInstallSheet, IconButton, type IconButtonProps, Img, type ImgProps, InAppSheet, type InAppSheetProps, InlineEdit, type InlineEditProps, Input, type InputProps, type IsoDuration, KitStyles, LinkifiedText, type LinkifiedTextProps, ListRow, type ListRowProps, MediaCard, type MediaCardProps, MessageList, type MessageListProps, PWAInstallButton, type PWAInstallState, PoweredByKit, type PoweredByKitProps, ProgressRing, type ProgressRingProps, Section, SectionHeader, type SectionHeaderProps, type SectionProps, type SegmentOption, SegmentedControl, type SegmentedControlProps, Select, type SelectOption, type SelectProps, ShareButton, type ShareButtonProps, Skeleton, type SkeletonProps, StatChip, type StatChipProps, type Step, Stepper, type StepperProps, Switch, type SwitchProps, THEME_SCRIPT, Tab, TabBar, type TabBarProps, type TabProps, type TabRootProps, Textarea, type TextareaProps, ThemeButton, type ThemeButtonProps, ThemeDialog, type ThemeDialogLabels, type ThemeDialogProps, type ToastOptions, ToastProvider, type ToastVariant, Tooltip, type TooltipProps, Typewriter, type TypewriterProps, TypingIndicator, type TypingIndicatorProps, type UnderlineTab, UnderlineTabs, type UnderlineTabsProps, type UseFetchOptions, type UseFetchResult, type UseFormSubmitOptions, type UseFormSubmitResult, type UseInViewOptions, type UseInViewResult, type UsePWAInstallReturn, type UsePollingOptions, type UsePollingResult, type UseShareOptions, type UseShareReturn, Watermark, type WatermarkProps, type WatermarkSponsor, clearFetchCache, cn, colors, createApiClient, createManifest, fillDateSeries, fontFamily, fonts, formatDayLabel, formatDuration, formatKoreanNumber, formatNumber, formatPrice, getFetchActiveCount, groupByDay, invalidateFetch, isoDurationToSec, mobileViewport, parseIsoDuration, postJson, relativeTime, subscribeFetchActivity, svgIcon, useCopy, useDebounce, useEscapeKey, useFetch, useFocusTrap, useFormSubmit, useImageLoader, useInView, useLocalStorage, usePWAInstall, usePolling, usePortalTarget, useScrollLock, useShare, useToast };
package/dist/index.d.ts CHANGED
@@ -71,6 +71,10 @@ interface AppShellProps {
71
71
  * strip) rather than a hand-rolled height/centering div; without a sized
72
72
  * parent the shell just collapses to content height.
73
73
  *
74
+ * The shell and the credit strip are centered together as one group — the
75
+ * strip sits directly under the shell and moves with it, it does not pin to
76
+ * the screen's true bottom edge.
77
+ *
74
78
  * @example
75
79
  * <Watermark color="#e2603f" text="myapp">
76
80
  * <AppShell>
@@ -84,8 +88,21 @@ interface AppShellHeaderProps {
84
88
  children: ReactNode;
85
89
  className?: string;
86
90
  }
91
+ /**
92
+ * `AppShellHeader`'s rendered height (base 3.5rem/56px + the top safe-area
93
+ * inset on notched devices). Pass this as `<FetchProgress top={APP_SHELL_HEADER_HEIGHT}>`
94
+ * instead of hardcoding `56` so the progress bar still sits flush under the
95
+ * header on a device with a Dynamic Island / notch.
96
+ */
97
+ declare const APP_SHELL_HEADER_HEIGHT = "calc(3.5rem + env(safe-area-inset-top))";
87
98
  /**
88
99
  * Sticky top header with blur backdrop.
100
+ *
101
+ * On mobile the shell is full-bleed (no `sm:` margin from Watermark), so this
102
+ * is the actual topmost element under the device status bar / Dynamic Island
103
+ * in an installed PWA — `min-height` (not `height`) plus `padding-top` let the
104
+ * safe-area inset grow the header without squeezing its content, the same
105
+ * ownership split PoweredByKit uses for the bottom inset.
89
106
  */
90
107
  declare function AppShellHeader({ children, className }: AppShellHeaderProps): react_jsx_runtime.JSX.Element;
91
108
  interface AppShellContentProps {
@@ -102,6 +119,8 @@ declare function AppShellContent({ children, className }: AppShellContentProps):
102
119
  interface FabProps {
103
120
  onClick: () => void;
104
121
  icon: ReactNode;
122
+ /** Accessible label — icon-only button has no other text for screen readers */
123
+ label: string;
105
124
  /** Accent background color. Default: zinc-900/white (follows dark mode) */
106
125
  color?: string;
107
126
  className?: string;
@@ -115,10 +134,10 @@ interface FabProps {
115
134
  * @example
116
135
  * <AppShellContent>
117
136
  * {content}
118
- * <Fab onClick={handleAdd} icon={<PlusIcon />} color="#6366f1" />
137
+ * <Fab onClick={handleAdd} icon={<PlusIcon />} label="추가" color="#6366f1" />
119
138
  * </AppShellContent>
120
139
  */
121
- declare function Fab({ onClick, icon, color, className }: FabProps): react_jsx_runtime.JSX.Element;
140
+ declare function Fab({ onClick, icon, label, color, className }: FabProps): react_jsx_runtime.JSX.Element;
122
141
 
123
142
  interface TabBarProps {
124
143
  children: ReactNode;
@@ -183,8 +202,13 @@ interface FetchProgressProps {
183
202
  * When omitted, the bar shows while ANY useFetch request is in flight.
184
203
  */
185
204
  active?: boolean;
186
- /** Distance from the top of the positioned ancestor, in px (e.g. header height). Default: 0 */
187
- top?: number;
205
+ /**
206
+ * Distance from the top of the positioned ancestor — a number of px, or any
207
+ * CSS length (e.g. `"calc(3.5rem + env(safe-area-inset-top))"` to sit below
208
+ * `AppShellHeader`, whose height grows with the safe-area inset on notched
209
+ * devices). Default: 0
210
+ */
211
+ top?: number | string;
188
212
  /** Bar thickness in px. Default: 3 */
189
213
  height?: number;
190
214
  className?: string;
@@ -1768,4 +1792,4 @@ declare function parseIsoDuration(duration: string): IsoDuration;
1768
1792
  /** "PT1H2M3S" → 3723 (초) */
1769
1793
  declare function isoDurationToSec(duration: string): number;
1770
1794
 
1771
- export { ActionCard, type ActionCardLabels, type ActionCardProps, type ActionCardState, type ApiClient, type ApiClientOptions, ApiError, AppShell, AppShellContent, type AppShellContentProps, AppShellHeader, type AppShellHeaderProps, type AppShellProps, AsyncList, type AsyncListProps, type AsyncListStatus, Avatar, type AvatarProps, Badge, type BadgeProps, BarList, type BarListItem, type BarListProps, Button, type ButtonProps, Carousel, type CarouselProps, ChatBubble, type ChatBubbleProps, type ChatMessage, type ChatRole, CodeBlock, type CodeBlockProps, Collapsible, type CollapsibleProps, type ColorName, ColorPicker, type ColorPickerProps, CopyButton, type CopyButtonProps, Countdown, type CountdownLabels, type CountdownProps, type DateCount, type DayGroup, Dialog, type DialogProps, Divider, type DividerProps, EmojiButton, type EmojiButtonProps, EmojiPicker, type EmojiPickerLabels, type EmojiPickerProps, EmptyState, type EmptyStateProps, Fab, type FabProps, FetchProgress, type FetchProgressProps, Field, type FieldProps, FontLinks, type FontName, GrassMap, type GrassMapData, type GrassMapLabels, type GrassMapProps, IOSInstallSheet, IconButton, type IconButtonProps, Img, type ImgProps, InAppSheet, type InAppSheetProps, InlineEdit, type InlineEditProps, Input, type InputProps, type IsoDuration, KitStyles, LinkifiedText, type LinkifiedTextProps, ListRow, type ListRowProps, MediaCard, type MediaCardProps, MessageList, type MessageListProps, PWAInstallButton, type PWAInstallState, PoweredByKit, type PoweredByKitProps, ProgressRing, type ProgressRingProps, Section, SectionHeader, type SectionHeaderProps, type SectionProps, type SegmentOption, SegmentedControl, type SegmentedControlProps, Select, type SelectOption, type SelectProps, ShareButton, type ShareButtonProps, Skeleton, type SkeletonProps, StatChip, type StatChipProps, type Step, Stepper, type StepperProps, Switch, type SwitchProps, THEME_SCRIPT, Tab, TabBar, type TabBarProps, type TabProps, type TabRootProps, Textarea, type TextareaProps, ThemeButton, type ThemeButtonProps, ThemeDialog, type ThemeDialogLabels, type ThemeDialogProps, type ToastOptions, ToastProvider, type ToastVariant, Tooltip, type TooltipProps, Typewriter, type TypewriterProps, TypingIndicator, type TypingIndicatorProps, type UnderlineTab, UnderlineTabs, type UnderlineTabsProps, type UseFetchOptions, type UseFetchResult, type UseFormSubmitOptions, type UseFormSubmitResult, type UseInViewOptions, type UseInViewResult, type UsePWAInstallReturn, type UsePollingOptions, type UsePollingResult, type UseShareOptions, type UseShareReturn, Watermark, type WatermarkProps, type WatermarkSponsor, clearFetchCache, cn, colors, createApiClient, createManifest, fillDateSeries, fontFamily, fonts, formatDayLabel, formatDuration, formatKoreanNumber, formatNumber, formatPrice, getFetchActiveCount, groupByDay, invalidateFetch, isoDurationToSec, mobileViewport, parseIsoDuration, postJson, relativeTime, subscribeFetchActivity, svgIcon, useCopy, useDebounce, useEscapeKey, useFetch, useFocusTrap, useFormSubmit, useImageLoader, useInView, useLocalStorage, usePWAInstall, usePolling, usePortalTarget, useScrollLock, useShare, useToast };
1795
+ export { APP_SHELL_HEADER_HEIGHT, ActionCard, type ActionCardLabels, type ActionCardProps, type ActionCardState, type ApiClient, type ApiClientOptions, ApiError, AppShell, AppShellContent, type AppShellContentProps, AppShellHeader, type AppShellHeaderProps, type AppShellProps, AsyncList, type AsyncListProps, type AsyncListStatus, Avatar, type AvatarProps, Badge, type BadgeProps, BarList, type BarListItem, type BarListProps, Button, type ButtonProps, Carousel, type CarouselProps, ChatBubble, type ChatBubbleProps, type ChatMessage, type ChatRole, CodeBlock, type CodeBlockProps, Collapsible, type CollapsibleProps, type ColorName, ColorPicker, type ColorPickerProps, CopyButton, type CopyButtonProps, Countdown, type CountdownLabels, type CountdownProps, type DateCount, type DayGroup, Dialog, type DialogProps, Divider, type DividerProps, EmojiButton, type EmojiButtonProps, EmojiPicker, type EmojiPickerLabels, type EmojiPickerProps, EmptyState, type EmptyStateProps, Fab, type FabProps, FetchProgress, type FetchProgressProps, Field, type FieldProps, FontLinks, type FontName, GrassMap, type GrassMapData, type GrassMapLabels, type GrassMapProps, IOSInstallSheet, IconButton, type IconButtonProps, Img, type ImgProps, InAppSheet, type InAppSheetProps, InlineEdit, type InlineEditProps, Input, type InputProps, type IsoDuration, KitStyles, LinkifiedText, type LinkifiedTextProps, ListRow, type ListRowProps, MediaCard, type MediaCardProps, MessageList, type MessageListProps, PWAInstallButton, type PWAInstallState, PoweredByKit, type PoweredByKitProps, ProgressRing, type ProgressRingProps, Section, SectionHeader, type SectionHeaderProps, type SectionProps, type SegmentOption, SegmentedControl, type SegmentedControlProps, Select, type SelectOption, type SelectProps, ShareButton, type ShareButtonProps, Skeleton, type SkeletonProps, StatChip, type StatChipProps, type Step, Stepper, type StepperProps, Switch, type SwitchProps, THEME_SCRIPT, Tab, TabBar, type TabBarProps, type TabProps, type TabRootProps, Textarea, type TextareaProps, ThemeButton, type ThemeButtonProps, ThemeDialog, type ThemeDialogLabels, type ThemeDialogProps, type ToastOptions, ToastProvider, type ToastVariant, Tooltip, type TooltipProps, Typewriter, type TypewriterProps, TypingIndicator, type TypingIndicatorProps, type UnderlineTab, UnderlineTabs, type UnderlineTabsProps, type UseFetchOptions, type UseFetchResult, type UseFormSubmitOptions, type UseFormSubmitResult, type UseInViewOptions, type UseInViewResult, type UsePWAInstallReturn, type UsePollingOptions, type UsePollingResult, type UseShareOptions, type UseShareReturn, Watermark, type WatermarkProps, type WatermarkSponsor, clearFetchCache, cn, colors, createApiClient, createManifest, fillDateSeries, fontFamily, fonts, formatDayLabel, formatDuration, formatKoreanNumber, formatNumber, formatPrice, getFetchActiveCount, groupByDay, invalidateFetch, isoDurationToSec, mobileViewport, parseIsoDuration, postJson, relativeTime, subscribeFetchActivity, svgIcon, useCopy, useDebounce, useEscapeKey, useFetch, useFocusTrap, useFormSubmit, useImageLoader, useInView, useLocalStorage, usePWAInstall, usePolling, usePortalTarget, useScrollLock, useShare, useToast };