ksk-design-system 1.45.0 → 1.46.0

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.
Files changed (65) hide show
  1. package/AGENTS.md +7 -0
  2. package/CLAUDE.md +7 -0
  3. package/DESIGN.md +3 -1
  4. package/PUBLISHING.md +32 -14
  5. package/README.md +83 -2
  6. package/UPDATING.md +15 -0
  7. package/bin/card-child-spacing.js +111 -0
  8. package/bin/check-duplicates.js +167 -0
  9. package/bin/init.js +12 -0
  10. package/bin/lint.js +10 -0
  11. package/contracts/components.json +240 -27
  12. package/contracts/composition.json +36 -9
  13. package/contracts/design-context.json +1 -0
  14. package/contracts/rules.json +34 -2
  15. package/contracts/token-hex-cache.json +1 -1
  16. package/dist/index.js +2511 -1875
  17. package/dist/native/ui.js +1375 -1272
  18. package/dist/{native-BelCzh9_.js → native-BYLnbCN-.js} +74 -1
  19. package/dist/native.js +1 -1
  20. package/dist/types/class-names.d.ts +1 -1
  21. package/dist/types/components/patterns/_internal/carousel-primitives.d.ts +24 -0
  22. package/dist/types/components/patterns/bottom-sheet-frame.d.ts +1 -1
  23. package/dist/types/components/patterns/coach-mark-overlay.d.ts +1 -1
  24. package/dist/types/components/patterns/content-carousel.d.ts +18 -0
  25. package/dist/types/components/patterns/empty-state.d.ts +1 -1
  26. package/dist/types/components/patterns/error-state.d.ts +8 -2
  27. package/dist/types/components/patterns/field.d.ts +23 -0
  28. package/dist/types/components/patterns/list-item.d.ts +22 -2
  29. package/dist/types/components/patterns/mobile-floating-action-button.d.ts +1 -1
  30. package/dist/types/components/patterns/screen.d.ts +4 -1
  31. package/dist/types/components/patterns/share-buttons.d.ts +7 -3
  32. package/dist/types/components/patterns/shells/admin-shell.d.ts +5 -1
  33. package/dist/types/components/patterns/shells/app-shell.d.ts +5 -1
  34. package/dist/types/components/patterns/shells/marketing-shell.d.ts +5 -1
  35. package/dist/types/components/patterns/side-drawer-frame.d.ts +1 -1
  36. package/dist/types/components/ui/alert-dialog.d.ts +1 -1
  37. package/dist/types/components/ui/auto-grow-textarea.d.ts +5 -2
  38. package/dist/types/components/ui/button.d.ts +1 -1
  39. package/dist/types/components/ui/container.d.ts +15 -0
  40. package/dist/types/components/ui/date-picker.d.ts +9 -2
  41. package/dist/types/components/ui/date-time-picker.d.ts +27 -0
  42. package/dist/types/components/ui/dialog.d.ts +1 -1
  43. package/dist/types/components/ui/icon-badge.d.ts +17 -0
  44. package/dist/types/components/ui/input.d.ts +7 -1
  45. package/dist/types/components/ui/section-nav.d.ts +28 -0
  46. package/dist/types/components/ui/section.d.ts +17 -0
  47. package/dist/types/components/ui/sheet.d.ts +1 -1
  48. package/dist/types/components/ui/skip-link.d.ts +8 -0
  49. package/dist/types/components/ui/textarea.d.ts +6 -1
  50. package/dist/types/components/ui/time-picker.d.ts +4 -1
  51. package/dist/types/index.d.ts +22 -2
  52. package/dist/types/lib/use-value-length.d.ts +25 -0
  53. package/dist/types/native/components/CheckboxField.d.ts +3 -1
  54. package/dist/types/native/components/DateTimePicker.d.ts +13 -0
  55. package/dist/types/native/components/IconBadge.d.ts +13 -0
  56. package/dist/types/native/components/Switch.d.ts +4 -1
  57. package/dist/types/native/components/index.d.ts +2 -0
  58. package/dist/types/tokens/native/scales.d.ts +73 -0
  59. package/package.json +15 -19
  60. package/src/components/COMPONENT_LOOKUP.md +10 -2
  61. package/src/native/COMPONENT_LOOKUP.md +3 -1
  62. package/src/preset.css +25 -0
  63. package/templates/AGENTS.md +15 -2
  64. package/templates/CLAUDE.md +15 -2
  65. package/tokens.json +78 -3
package/AGENTS.md CHANGED
@@ -38,6 +38,13 @@ UI コンポーネント・画面の生成/修正・レビューの前には、
38
38
 
39
39
  コンポーネントを新規作成する前に `COMPONENT_LOOKUP.md` で同等品がないか確認すること。
40
40
 
41
+ ### ローカル二重実装ゲート
42
+
43
+ DS に無いと思っても consumer 側に別台帳を作らないこと。最初に `contracts/components.json` と
44
+ `COMPONENT_LOOKUP.md` を検索し、consumer では `npx ksk-ds check-duplicates ./src --strict` を実行する。
45
+ それでも不足する場合は DS 側に issue を登録する。やむを得ない一時実装には、削除条件と issue を
46
+ `// ksk-ds-local-fallback: DS に X が追加されたら削除 (issue #123)` の形式で残すこと。
47
+
41
48
  Storybook 全体を横断で視覚監査する(定期監査・リリース前総点検)場合は `.claude/skills/audit-pages/SKILL.md` の手順に従うこと。
42
49
 
43
50
  ---
package/CLAUDE.md CHANGED
@@ -92,6 +92,13 @@ Brand色を差し替え(10行)→ Primitive Layer → Semantic Layer → Bri
92
92
  5. `tokens.json` でカラー・余白・影・タイポのトークンを確認
93
93
  6. 画面(ページ/ダイアログ等)を実装する場合は `contracts/screen-patterns.json` の decisionTree でシェル/パターンを選び、`contracts/composition.json` で内部の並べ方を確認
94
94
 
95
+ ### ローカル二重実装ゲート
96
+
97
+ DS に無いと思っても consumer 側に別台帳を作らないこと。最初に `contracts/components.json` と
98
+ `COMPONENT_LOOKUP.md` を検索し、consumer では `npx ksk-ds check-duplicates ./src --strict` を実行する。
99
+ それでも不足する場合は DS 側に issue を登録する。やむを得ない一時実装には、削除条件と issue を
100
+ `// ksk-ds-local-fallback: DS に X が追加されたら削除 (issue #123)` の形式で残すこと。
101
+
95
102
  **`.tsx` を編集したら `bash scripts/lint-scratch.sh`、コンポーネント増減時は `npm run check` を実行すること。**
96
103
 
97
104
  Storybook 全体を横断で視覚監査する(定期監査・リリース前総点検・「全ページ確認して」)場合は `.claude/skills/audit-pages/SKILL.md` を使う。
package/DESIGN.md CHANGED
@@ -46,6 +46,7 @@ rounded:
46
46
  spacing:
47
47
  unit: "4px" # 4px グリッド(scale: 0,4,8,12,16,20,24,28,32,36,40,44,48,...)
48
48
  page: "16px" # 基準画面端マージン。実レイアウトは Screen / Shell の padding contract を優先
49
+ section: { xs: "32px", sm: "40px", md: "48px", lg: "56px", xl: "64px", 2xl: "80px" }
49
50
  elevation: # 影色は neutral(Gray-900 ベース rgba(17,24,39,…))でテーマ非依存
50
51
  sm: "0 1px 2px 0 rgba(0, 0, 0, 0.05)"
51
52
  md: "0 0 8px rgba(20, 20, 20, 0.08)"
@@ -98,7 +99,7 @@ EC/BtoC 系)を回すのが狙い。
98
99
  `DESIGN.md` は Google DESIGN.md の「front matter + rationale」形式を参考にした **AI 向け配布サマリ**であり、
99
100
  実装正本ではない。KSK の正本は次のファイルに置く。
100
101
 
101
- - `tokens.json`: primitive / semantic / dark semantic / typography / spacing / shadow / touch target。
102
+ - `tokens.json`: primitive / semantic / dark semantic / typography / spacing / responsive breakpoint / shadow / touch target。
102
103
  - `src/styles/*.css`: 実際に publish される CSS custom properties と typography / glass utilities。
103
104
  - `contracts/rules.json`: 禁止パターン、AI anti-pattern、a11y、consumer lint の正本。
104
105
  - `contracts/components.json`: component 名、variant、subcomponent、usage rule、件数の正本。
@@ -140,6 +141,7 @@ KSK の必須正本・publish 依存にせず、KSK 固有の multi-theme / nati
140
141
  ## Layout & Spacing
141
142
 
142
143
  - **4px グリッド**。余白・サイズは 4 の倍数(scale 0–60)。
144
+ - セクション同士の縦リズムは `--Space-Section-{xs..2xl}`(32–80px)を使い、コンポーネント内の gap scale と分離する。
143
145
  - 画面端マージン: 16px を基準にしつつ、実レイアウトでは `Screen` / shell component の padding contract を優先する。
144
146
  - **タッチターゲット**(モバイル): WCAG 2.5.5 / Apple HIG に従い主要操作(ボタン/アイコンボタン/入力/ナビ)の
145
147
  **min は 44px** 以上、推奨 48px。44 未満が避けられない **チップ(min 32px)は hitSlop**(不可視の拡張タップ領域)で
package/PUBLISHING.md CHANGED
@@ -5,7 +5,8 @@
5
5
  > **配布方式**: v1.36.0 以降は npm レジストリ経由で配布する。
6
6
  > `npm publish --access public` で公開し、各消費リポジトリの
7
7
  > `package.json` は `ksk-design-system@X.Y.Z` を参照する。
8
- > 消費リポ: belle-todo / trip_todo / ninshin-todo / yokoku-app / pawly(いずれも `~/LocalDev/` 直下)。
8
+ > 対象の消費リポ一覧は `scripts/update-consumers.sh` `DEFAULT_REPOS` が正本
9
+ > (単体リポ・monorepo が混在し、`~/LocalDev/` と `~/LocalDev/Examination/` 配下にまたがる)。
9
10
 
10
11
  ## 前提
11
12
 
@@ -15,18 +16,32 @@
15
16
  ## 最短手順(推奨)
16
17
 
17
18
  ```bash
18
- bash scripts/release.sh minor # patch / minor / major / x.y.z
19
+ # release branch package.json / package-lock.json と契約 version を更新
20
+ # PR を main にマージ
21
+ RUN_ID="$(gh run list --workflow=publish.yml --branch=main --limit=1 \
22
+ --json databaseId --jq '.[0].databaseId')"
23
+ gh run watch "$RUN_ID" --exit-status
19
24
  ```
20
25
 
21
- これだけ。中で以下を全部やる:
26
+ main への version 変更を `.github/workflows/publish.yml` が検知し、npm Trusted
27
+ Publishing (OIDC) で以下を自動実行する:
22
28
 
23
- 1. `git diff --quiet` & main ブランチチェック(曜日チェック)
24
- 2. `npm run check`
25
- 3. `npm version <level>`(tag 切り)
26
- 4. `npm pack`(prepack `dist/` を生成し、中身を検証)
27
- 5. `npm publish --access public`
28
- 6. `git push origin main --tags`
29
- 7. `bash scripts/update-consumers.sh <version>`(5 リポへ PR 自動作成)
29
+ 1. npm 上の最新版と `package.json` version を比較
30
+ 2. `npm ci`
31
+ 3. `npm publish`(prepack `dist/` を生成)
32
+ 4. `vX.Y.Z` tag GitHub Release を作成
33
+
34
+ 公開後、レジストリ反映を確認してから消費リポへ配布する:
35
+
36
+ ```bash
37
+ npm view ksk-design-system@<version> version --json
38
+ npm dist-tag ls ksk-design-system
39
+ bash scripts/update-consumers.sh <version>
40
+ ```
41
+
42
+ PR では `npm run check`、`npm test`、`npm pack --dry-run` を通してからマージする。
43
+ ローカル認証による手動公開が必要な場合だけ `bash scripts/release.sh <version>` を
44
+ フォールバックとして使う。
30
45
 
31
46
  > v1.35.0 で旧名 `@ksk/design-system` 互換 tgz の生成は廃止。
32
47
  > 消費5リポ + todo-shared が新名 `ksk-design-system` に移行済。
@@ -148,16 +163,19 @@ bash scripts/update-consumers.sh <version> <影響リポ...>
148
163
  ## 注意
149
164
 
150
165
  - 配布前に必ず `npm pack --dry-run` で中身確認
151
- - npm registry 公開には `npm login` 済みであること
166
+ - 通常公開は Trusted Publishing (OIDC) を使う。`npm login`
167
+ `scripts/release.sh` でローカル公開へフォールバックする場合のみ必要
152
168
  - `package.json#exports` を変更したら必ず利用側プロジェクトでの import を試す
153
169
  - 金曜午後のリリースは厳禁(週末に障害対応できない)
154
170
  - メジャーリリースは月初の月曜が望ましい(フィードバック収集期間が取れる)
155
171
 
156
172
  ## npm 公開について
157
173
 
158
- v1.36.0 以降は npm registry 経由配布。GitHub Actions による自動 publish はなく、
159
- ローカルの `npm login` 済み環境から `scripts/release.sh` で公開する。
160
- CI/CD に戻す場合は、NPM_TOKEN Secrets 登録と workflow の再作成をセットで行うこと。
174
+ v1.36.0 以降は npm registry 経由配布。通常公開は
175
+ `.github/workflows/publish.yml` npm Trusted Publishing (OIDC) を使うため、
176
+ 長寿命の `NPM_TOKEN` やローカルの `npm login` は不要。
177
+ workflow が利用できない緊急時だけ、`scripts/release.sh` のローカル公開へ
178
+ フォールバックする。
161
179
 
162
180
  ## 関連
163
181
 
package/README.md CHANGED
@@ -37,7 +37,7 @@ Brand 色を差し替えるだけで業種に合わせた配色に切り替わ
37
37
 
38
38
  React 19 + TypeScript / Vite / **Tailwind CSS v4** / shadcn/ui(Radix UI)/ CVA / iconsax-reactjs / Storybook
39
39
 
40
- **Peer dependencies**: `react` 18 or 19, `react-dom`, `tailwindcss` ^4, `radix-ui`, `@radix-ui/react-slot`, `iconsax-reactjs`
40
+ **Peer dependencies**: `react` 18 or 19, `react-dom`, `tailwindcss` ^4
41
41
 
42
42
  ## 🚀 使い方
43
43
 
@@ -48,11 +48,18 @@ npm install ksk-design-system
48
48
  ```
49
49
 
50
50
  ```css
51
- /* プロジェクトの CSS */
51
+ /* globals.css / app.css(CSS の場所に応じて ../../ の数を調整) */
52
+ @import "tailwindcss";
52
53
  @import "ksk-design-system/preset";
54
+ @import "ksk-design-system/themes/default";
53
55
  @import "./themes/my-client.css"; /* Brand 色を差し替えたテーマ */
56
+ @source "../../node_modules/ksk-design-system/dist";
54
57
  ```
55
58
 
59
+ Tailwind CSS v4 は `node_modules` を既定では走査しません。`@source` がないと、
60
+ DS 内部だけで使うレイアウト・サイズ・状態クラスが生成されず、コンポーネントの表示や操作が崩れます。
61
+ consumer 側の Tailwind と DS を同じビルドで処理するため、上記の設定をセットで使用してください。
62
+
56
63
  ```tsx
57
64
  import { Button, Card, Input, FormField } from "ksk-design-system"
58
65
  ```
@@ -75,6 +82,80 @@ npx ksk-ds lint --changed
75
82
  // ksk-ds-allow-custom-ui: medical chart requires bespoke interaction
76
83
  ```
77
84
 
85
+ ### Jest(CommonJS)でコンポーネントをテストする
86
+
87
+ このパッケージは **ESM-only** です。CJS との dual build は配布せず、Jest
88
+ 側で DS とその ESM 依存を `babel-jest` の変換対象にします。Vitest など
89
+ ESM をネイティブに扱うランナーでは、以下の設定は不要です。
90
+
91
+ Jest 29 では Babel 7 系を使います(Babel 8 は `babel-jest@29` の peer
92
+ 範囲外です)。
93
+
94
+ ```bash
95
+ npm install --save-dev \
96
+ jest@29.7.0 babel-jest@29.7.0 jest-environment-jsdom@29.7.0 \
97
+ @babel/core@^7.28.0 @babel/preset-env@^7.28.0 \
98
+ @babel/preset-react@^7.28.0 @babel/preset-typescript@^7.28.0
99
+ ```
100
+
101
+ ```js
102
+ // babel.config.cjs
103
+ module.exports = {
104
+ presets: [
105
+ ["@babel/preset-env", { targets: { node: "current" } }],
106
+ ["@babel/preset-react", { runtime: "automatic" }],
107
+ ["@babel/preset-typescript", { allExtensions: true, isTSX: true }],
108
+ ],
109
+ }
110
+ ```
111
+
112
+ ```js
113
+ // jest.config.cjs
114
+ module.exports = {
115
+ testEnvironment: "jsdom",
116
+ transform: {
117
+ "^.+\\.m?[jt]sx?$": "babel-jest",
118
+ },
119
+ transformIgnorePatterns: [
120
+ "<rootDir>/node_modules/.pnpm/(?!(ksk-design-system|radix-ui|iconsax-reactjs|@radix-ui\\+[^@]+)@)",
121
+ "node_modules/(?!.pnpm|ksk-design-system|radix-ui|@radix-ui|iconsax-reactjs)",
122
+ ],
123
+ moduleNameMapper: {
124
+ "\\.(css|less|sass|scss)$": "<rootDir>/test/style-mock.cjs",
125
+ "^ksk-design-system/(preset|styles(?:\\.css)?|glass|tokens/(?:primitive|semantic|typography|categorical)|themes/(?:default|blue|orange|green|violet|cobalt))$":
126
+ "<rootDir>/test/style-mock.cjs",
127
+ },
128
+ }
129
+ ```
130
+
131
+ ```js
132
+ // test/style-mock.cjs
133
+ module.exports = {}
134
+ ```
135
+
136
+ この構成なら `ksk-design-system` をコンポーネント単位で mock せず、そのまま
137
+ render できます。リポジトリ内では、実際に `npm pack` した tgz を空の Jest
138
+ プロジェクトへインストールする再現テストを実行できます。
139
+
140
+ ```bash
141
+ npm run test:jest-consumer
142
+ ```
143
+
144
+ React Native / Expo の `jest-expo` でも考え方は同じです。既存の Expo preset
145
+ は維持し、`transformIgnorePatterns` の除外対象へ
146
+ `ksk-design-system` と利用する ESM peer を加えてください。
147
+
148
+ ### Consumer duplicate check
149
+
150
+ DS に存在する部品を consumer 側で再実装しないよう、コンポーネント名の重複検査を同梱しています。
151
+
152
+ ```bash
153
+ npx ksk-ds check-duplicates
154
+ npx ksk-ds check-duplicates ./src --strict
155
+ ```
156
+
157
+ 既定は助言モードで終了コード 0、`--strict` は重複候補があると終了コード 1 です。正本は同梱の `contracts/components.json` であり、consumer 側に別の「昇格候補台帳」を作らないでください。
158
+
78
159
  ### Media overlay utilities
79
160
 
80
161
  動画・写真の上に文字や操作を置く場合は、`--Text-on-Media` と `.text-on-media` / `.text-on-media-secondary`、上下の `.media-scrim-top` / `.media-scrim-bottom` を使います。TikTok / Reels 型の操作群は `MediaActionCluster` が glass ボタン、ラベル、safe-area anchor、idle auto-hide をまとめて扱います。
package/UPDATING.md CHANGED
@@ -85,6 +85,21 @@ gh pr create
85
85
  手動移行が必要な項目(codemod で拾いきれないもの)は [MIGRATION.md](./MIGRATION.md) の
86
86
  「移行作業の進め方」節を参照。
87
87
 
88
+ ### Tailwind CSS v4 の走査設定
89
+
90
+ アップデート後も、consumer のエントリCSSには次の構成を維持する。
91
+ CSSファイルの場所に応じて `../../` の数だけ調整する。
92
+
93
+ ```css
94
+ @import "tailwindcss";
95
+ @import "ksk-design-system/preset";
96
+ @import "ksk-design-system/themes/default";
97
+ @source "../../node_modules/ksk-design-system/dist";
98
+ ```
99
+
100
+ `@source` を外すと、DS内部だけで使われるクラスが生成されない。`dist` のプリビルドCSSと
101
+ consumer側Tailwindを重ねる二重ビルド方式にも切り替えないこと。
102
+
88
103
  ## 5. していい・ダメ早見表
89
104
 
90
105
  | していい | ダメ |
@@ -0,0 +1,111 @@
1
+ import ts from "typescript"
2
+
3
+ const SPACING_CLASS =
4
+ /(?:^|[\s"'`:({[])-?(?:mt|mb|my|space-y)-[^\s"'`})\]]+/
5
+
6
+ function tagNameOf(node, sourceFile) {
7
+ if (ts.isJsxElement(node)) {
8
+ return node.openingElement.tagName.getText(sourceFile)
9
+ }
10
+ if (ts.isJsxSelfClosingElement(node)) {
11
+ return node.tagName.getText(sourceFile)
12
+ }
13
+ return ""
14
+ }
15
+
16
+ function openingOf(node) {
17
+ if (ts.isJsxElement(node)) return node.openingElement
18
+ if (ts.isJsxSelfClosingElement(node)) return node
19
+ return null
20
+ }
21
+
22
+ function hasMediaVariant(opening, sourceFile) {
23
+ const attribute = opening.attributes.properties.find(
24
+ (property) =>
25
+ ts.isJsxAttribute(property) &&
26
+ property.name.getText(sourceFile) === "variant",
27
+ )
28
+ if (!attribute || !ts.isJsxAttribute(attribute) || !attribute.initializer) {
29
+ return false
30
+ }
31
+ if (ts.isStringLiteral(attribute.initializer)) {
32
+ return attribute.initializer.text === "media"
33
+ }
34
+ if (
35
+ ts.isJsxExpression(attribute.initializer) &&
36
+ attribute.initializer.expression &&
37
+ (ts.isStringLiteral(attribute.initializer.expression) ||
38
+ ts.isNoSubstitutionTemplateLiteral(attribute.initializer.expression))
39
+ ) {
40
+ return attribute.initializer.expression.text === "media"
41
+ }
42
+ return false
43
+ }
44
+
45
+ function spacingClassOf(node, sourceFile) {
46
+ const opening = openingOf(node)
47
+ if (!opening) return null
48
+ const className = opening.attributes.properties.find(
49
+ (property) =>
50
+ ts.isJsxAttribute(property) &&
51
+ property.name.getText(sourceFile) === "className",
52
+ )
53
+ if (!className || !ts.isJsxAttribute(className) || !className.initializer) {
54
+ return null
55
+ }
56
+ const raw = className.initializer.getText(sourceFile)
57
+ return raw.match(SPACING_CLASS)?.[0]?.trim() ?? null
58
+ }
59
+
60
+ function directJsxRoots(node, visit) {
61
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
62
+ visit(node)
63
+ return
64
+ }
65
+ if (ts.isJsxFragment(node)) {
66
+ for (const child of node.children) directJsxRoots(child, visit)
67
+ return
68
+ }
69
+ if (ts.isJsxExpression(node) && !node.expression) return
70
+ ts.forEachChild(node, (child) => directJsxRoots(child, visit))
71
+ }
72
+
73
+ export function inspectCardChildSpacing(source, filePath = "source.tsx") {
74
+ const sourceFile = ts.createSourceFile(
75
+ filePath,
76
+ source,
77
+ ts.ScriptTarget.Latest,
78
+ true,
79
+ ts.ScriptKind.TSX,
80
+ )
81
+ const findings = []
82
+
83
+ function walk(node) {
84
+ if (
85
+ ts.isJsxElement(node) &&
86
+ tagNameOf(node, sourceFile) === "Card" &&
87
+ !hasMediaVariant(node.openingElement, sourceFile)
88
+ ) {
89
+ for (const child of node.children) {
90
+ directJsxRoots(child, (root) => {
91
+ const spacingClass = spacingClassOf(root, sourceFile)
92
+ if (!spacingClass) return
93
+ const position = sourceFile.getLineAndCharacterOfPosition(
94
+ root.getStart(),
95
+ )
96
+ findings.push({
97
+ line: position.line + 1,
98
+ tag: tagNameOf(root, sourceFile),
99
+ spacingClass,
100
+ })
101
+ })
102
+ }
103
+ ts.forEachChild(node, walk)
104
+ return
105
+ }
106
+ ts.forEachChild(node, walk)
107
+ }
108
+
109
+ walk(sourceFile)
110
+ return findings
111
+ }
@@ -0,0 +1,167 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"
2
+ import { extname, join, relative, resolve } from "node:path"
3
+
4
+ const SOURCE_EXTENSIONS = new Set([".ts", ".tsx"])
5
+ const IGNORED_DIRECTORIES = new Set([
6
+ ".git",
7
+ ".next",
8
+ "build",
9
+ "coverage",
10
+ "dist",
11
+ "node_modules",
12
+ "storybook-static",
13
+ ])
14
+
15
+ export function runCheckDuplicatesCli(
16
+ argv,
17
+ { cwd = process.cwd(), pkgRoot = resolve(".") } = {},
18
+ ) {
19
+ const options = parseArgs(argv)
20
+ if (options.help) {
21
+ printHelp()
22
+ return 0
23
+ }
24
+
25
+ const contractsPath = resolve(pkgRoot, "contracts/components.json")
26
+ if (!existsSync(contractsPath)) {
27
+ console.error(`contracts/components.json が見つかりません: ${contractsPath}`)
28
+ return 1
29
+ }
30
+
31
+ const target = resolve(cwd, options.target)
32
+ if (!existsSync(target)) {
33
+ console.error(`検査対象が見つかりません: ${target}`)
34
+ return 1
35
+ }
36
+
37
+ const registry = loadComponentRegistry(contractsPath)
38
+ const files = collectSourceFiles(target)
39
+ const findings = files
40
+ .flatMap((file) => findDuplicateExports(file, cwd, registry))
41
+ .sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.name.localeCompare(b.name))
42
+
43
+ printFindings(findings, files.length, options.strict)
44
+ return options.strict && findings.length > 0 ? 1 : 0
45
+ }
46
+
47
+ function parseArgs(argv) {
48
+ let strict = false
49
+ let help = false
50
+ let target = "./src"
51
+
52
+ for (const arg of argv) {
53
+ if (arg === "--strict") {
54
+ strict = true
55
+ continue
56
+ }
57
+ if (arg === "--help" || arg === "-h") {
58
+ help = true
59
+ continue
60
+ }
61
+ if (!arg.startsWith("-")) target = arg
62
+ }
63
+
64
+ return { help, strict, target }
65
+ }
66
+
67
+ function loadComponentRegistry(contractsPath) {
68
+ const contracts = JSON.parse(readFileSync(contractsPath, "utf8"))
69
+ const registry = new Map()
70
+
71
+ for (const tier of ["ui", "patterns", "commerce", "admin", "shells"]) {
72
+ for (const component of contracts[tier] ?? []) {
73
+ if (typeof component?.name !== "string" || typeof component?.path !== "string") continue
74
+ const matches = registry.get(component.name) ?? []
75
+ matches.push({ tier, path: component.path })
76
+ registry.set(component.name, matches)
77
+ }
78
+ }
79
+
80
+ return registry
81
+ }
82
+
83
+ function collectSourceFiles(target) {
84
+ const files = []
85
+
86
+ function walk(path) {
87
+ const stat = statSync(path)
88
+ if (stat.isDirectory()) {
89
+ for (const entry of readdirSync(path, { withFileTypes: true })) {
90
+ if (entry.isDirectory() && IGNORED_DIRECTORIES.has(entry.name)) continue
91
+ walk(join(path, entry.name))
92
+ }
93
+ return
94
+ }
95
+ if (stat.isFile() && SOURCE_EXTENSIONS.has(extname(path))) files.push(path)
96
+ }
97
+
98
+ walk(target)
99
+ return files.sort()
100
+ }
101
+
102
+ function findDuplicateExports(file, cwd, registry) {
103
+ const source = readFileSync(file, "utf8")
104
+ const lines = source.split(/\r?\n/)
105
+ const findings = []
106
+ const declarationPattern =
107
+ /^\s*export\s+(?:default\s+)?(?:(?:async|declare)\s+)?(?:function|const)\s+([A-Z][A-Za-z0-9]*)\b/
108
+
109
+ for (let index = 0; index < lines.length; index += 1) {
110
+ const match = lines[index].match(declarationPattern)
111
+ if (!match) continue
112
+ const name = match[1]
113
+ const components = registry.get(name)
114
+ if (!components) continue
115
+ findings.push({
116
+ components,
117
+ file: normalize(relative(cwd, file)),
118
+ line: index + 1,
119
+ name,
120
+ })
121
+ }
122
+
123
+ return findings
124
+ }
125
+
126
+ function printFindings(findings, scannedFiles, strict) {
127
+ if (findings.length === 0) {
128
+ console.log(`ksk-ds check-duplicates: 重複候補はありません(${scannedFiles} files)`)
129
+ return
130
+ }
131
+
132
+ console.log(`ksk-ds check-duplicates: DS と同名のローカル実装を ${findings.length} 件検出しました`)
133
+ for (const finding of findings) {
134
+ console.log(`\n${finding.file}:${finding.line} ${finding.name}`)
135
+ for (const component of finding.components) {
136
+ console.log(` DS: ${component.tier} / ${component.path}`)
137
+ }
138
+ }
139
+ console.log("\nローカル実装を削除して ksk-design-system から import できないか確認してください。")
140
+ console.log("DS に必要な機能が不足する場合は、ローカル台帳を作らず DS 側に Issue を登録してください。")
141
+ console.log(
142
+ strict
143
+ ? "strict mode: 重複候補があるため終了コード 1"
144
+ : "advice mode: 終了コード 0(CI で失敗させる場合は --strict)",
145
+ )
146
+ }
147
+
148
+ function printHelp() {
149
+ console.log(`ksk-ds check-duplicates
150
+
151
+ 使い方:
152
+ ksk-ds check-duplicates [DIR]
153
+ ksk-ds check-duplicates [DIR] --strict
154
+
155
+ DIR の既定値:
156
+ ./src
157
+
158
+ 動作:
159
+ export function / export const の PascalCase 宣言を
160
+ contracts/components.json と名前の完全一致で照合します。
161
+ 既定は助言モード(常に exit 0)、--strict は検出時 exit 1 です。
162
+ `)
163
+ }
164
+
165
+ function normalize(path) {
166
+ return path.split("\\").join("/")
167
+ }
package/bin/init.js CHANGED
@@ -11,6 +11,7 @@
11
11
  // npx ksk-design-system init --force # 既存ファイルを上書き
12
12
  // npx ksk-design-system demo [dir] # DS リポを clone + setup(お試し用)
13
13
  // npx ksk-ds lint src # contracts/rules.json に基づき consumer UI を検査
14
+ // npx ksk-ds check-duplicates src # DS と同名のローカル実装を検査
14
15
  // npx ksk-design-system postinstall # npm postinstall から呼ばれる silent モード
15
16
 
16
17
  import { copyFileSync, existsSync } from "node:fs"
@@ -37,6 +38,8 @@ if (cmd === "help" || cmd === "--help" || cmd === "-h") {
37
38
  npx ksk-ds lint src --format json CI 向け JSON 出力
38
39
  npx ksk-ds lint --changed Git 差分のみ検査
39
40
  npx ksk-ds check-migration ./src 非推奨 API の残存を検査(read-only)
41
+ npx ksk-ds check-duplicates [DIR] DS と同名のローカル実装を検査
42
+ npx ksk-ds check-duplicates --strict 検出時に exit 1(CI 向け)
40
43
  `)
41
44
  process.exit(0)
42
45
  }
@@ -53,6 +56,15 @@ if (cmd === "check-migration") {
53
56
  process.exit(status)
54
57
  }
55
58
 
59
+ if (cmd === "check-duplicates") {
60
+ const { runCheckDuplicatesCli } = await import("./check-duplicates.js")
61
+ const status = runCheckDuplicatesCli(args.slice(1), {
62
+ cwd: process.cwd(),
63
+ pkgRoot,
64
+ })
65
+ process.exit(status)
66
+ }
67
+
56
68
  if (cmd === "demo") {
57
69
  runDemo(args.slice(1))
58
70
  process.exit(0)
package/bin/lint.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"
2
2
  import { extname, join, relative, resolve } from "node:path"
3
3
  import { spawnSync } from "node:child_process"
4
+ import { inspectCardChildSpacing } from "./card-child-spacing.js"
4
5
 
5
6
  const DEFAULT_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx"])
6
7
  const DEFAULT_IGNORES = [
@@ -166,6 +167,15 @@ function lintFile(file, cwd, rules) {
166
167
  const lines = source.split(/\r?\n/)
167
168
 
168
169
  for (const rule of rules) {
170
+ if (rule.engine === "card-direct-child-spacing") {
171
+ for (const finding of inspectCardChildSpacing(source, file)) {
172
+ const line = lines[finding.line - 1] ?? ""
173
+ if (!matchesRuleExclude(rule, rel, line)) {
174
+ findings.push(toFinding(rule, rel, finding.line))
175
+ }
176
+ }
177
+ continue
178
+ }
169
179
  let regex
170
180
  try {
171
181
  regex = new RegExp(rule.pattern)