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.
- package/AGENTS.md +7 -0
- package/CLAUDE.md +7 -0
- package/DESIGN.md +3 -1
- package/PUBLISHING.md +32 -14
- package/README.md +83 -2
- package/UPDATING.md +15 -0
- package/bin/card-child-spacing.js +111 -0
- package/bin/check-duplicates.js +167 -0
- package/bin/init.js +12 -0
- package/bin/lint.js +10 -0
- package/contracts/components.json +240 -27
- package/contracts/composition.json +36 -9
- package/contracts/design-context.json +1 -0
- package/contracts/rules.json +34 -2
- package/contracts/token-hex-cache.json +1 -1
- package/dist/index.js +2511 -1875
- package/dist/native/ui.js +1375 -1272
- package/dist/{native-BelCzh9_.js → native-BYLnbCN-.js} +74 -1
- package/dist/native.js +1 -1
- package/dist/types/class-names.d.ts +1 -1
- package/dist/types/components/patterns/_internal/carousel-primitives.d.ts +24 -0
- package/dist/types/components/patterns/bottom-sheet-frame.d.ts +1 -1
- package/dist/types/components/patterns/coach-mark-overlay.d.ts +1 -1
- package/dist/types/components/patterns/content-carousel.d.ts +18 -0
- package/dist/types/components/patterns/empty-state.d.ts +1 -1
- package/dist/types/components/patterns/error-state.d.ts +8 -2
- package/dist/types/components/patterns/field.d.ts +23 -0
- package/dist/types/components/patterns/list-item.d.ts +22 -2
- package/dist/types/components/patterns/mobile-floating-action-button.d.ts +1 -1
- package/dist/types/components/patterns/screen.d.ts +4 -1
- package/dist/types/components/patterns/share-buttons.d.ts +7 -3
- package/dist/types/components/patterns/shells/admin-shell.d.ts +5 -1
- package/dist/types/components/patterns/shells/app-shell.d.ts +5 -1
- package/dist/types/components/patterns/shells/marketing-shell.d.ts +5 -1
- package/dist/types/components/patterns/side-drawer-frame.d.ts +1 -1
- package/dist/types/components/ui/alert-dialog.d.ts +1 -1
- package/dist/types/components/ui/auto-grow-textarea.d.ts +5 -2
- package/dist/types/components/ui/button.d.ts +1 -1
- package/dist/types/components/ui/container.d.ts +15 -0
- package/dist/types/components/ui/date-picker.d.ts +9 -2
- package/dist/types/components/ui/date-time-picker.d.ts +27 -0
- package/dist/types/components/ui/dialog.d.ts +1 -1
- package/dist/types/components/ui/icon-badge.d.ts +17 -0
- package/dist/types/components/ui/input.d.ts +7 -1
- package/dist/types/components/ui/section-nav.d.ts +28 -0
- package/dist/types/components/ui/section.d.ts +17 -0
- package/dist/types/components/ui/sheet.d.ts +1 -1
- package/dist/types/components/ui/skip-link.d.ts +8 -0
- package/dist/types/components/ui/textarea.d.ts +6 -1
- package/dist/types/components/ui/time-picker.d.ts +4 -1
- package/dist/types/index.d.ts +22 -2
- package/dist/types/lib/use-value-length.d.ts +25 -0
- package/dist/types/native/components/CheckboxField.d.ts +3 -1
- package/dist/types/native/components/DateTimePicker.d.ts +13 -0
- package/dist/types/native/components/IconBadge.d.ts +13 -0
- package/dist/types/native/components/Switch.d.ts +4 -1
- package/dist/types/native/components/index.d.ts +2 -0
- package/dist/types/tokens/native/scales.d.ts +73 -0
- package/package.json +15 -19
- package/src/components/COMPONENT_LOOKUP.md +10 -2
- package/src/native/COMPONENT_LOOKUP.md +3 -1
- package/src/preset.css +25 -0
- package/templates/AGENTS.md +15 -2
- package/templates/CLAUDE.md +15 -2
- 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
|
-
>
|
|
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
|
-
|
|
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.
|
|
24
|
-
2. `npm
|
|
25
|
-
3. `npm
|
|
26
|
-
4. `
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
-
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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
|
-
/*
|
|
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)
|