ksk-design-system 1.38.0 → 1.40.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 (44) hide show
  1. package/NATIVE_RECIPES.md +141 -0
  2. package/README.md +42 -3
  3. package/bin/init.js +10 -0
  4. package/bin/lint.js +266 -0
  5. package/contracts/components.json +292 -18
  6. package/contracts/rules.json +100 -1
  7. package/dist/index.js +4802 -3582
  8. package/dist/native/ui.js +3450 -2229
  9. package/dist/types/components/patterns/admin/data-table.d.ts +73 -9
  10. package/dist/types/components/patterns/bottom-sheet-frame.d.ts +8 -0
  11. package/dist/types/components/patterns/celebration.d.ts +6 -1
  12. package/dist/types/components/patterns/commerce/bottom-tab-bar.d.ts +34 -2
  13. package/dist/types/components/patterns/compact-file-picker.d.ts +27 -0
  14. package/dist/types/components/patterns/detail-sheet-scaffold.d.ts +17 -0
  15. package/dist/types/components/patterns/empty-state.d.ts +7 -1
  16. package/dist/types/components/patterns/keyboard-aware-sheet-footer.d.ts +15 -0
  17. package/dist/types/components/patterns/mobile-app-header.d.ts +13 -0
  18. package/dist/types/components/patterns/mobile-app-shell.d.ts +18 -0
  19. package/dist/types/components/patterns/mobile-floating-action-button.d.ts +17 -0
  20. package/dist/types/components/patterns/quick-action-grid.d.ts +22 -0
  21. package/dist/types/components/patterns/settings-section.d.ts +22 -0
  22. package/dist/types/components/ui/auto-grow-textarea.d.ts +6 -2
  23. package/dist/types/components/ui/status-action-badge.d.ts +15 -0
  24. package/dist/types/components/ui/toast.d.ts +4 -0
  25. package/dist/types/index.d.ts +24 -4
  26. package/dist/types/lib/use-visual-viewport-keyboard-inset.d.ts +8 -0
  27. package/dist/types/native/components/AutoGrowTextarea.d.ts +3 -1
  28. package/dist/types/native/components/BottomSheetFrame.d.ts +13 -0
  29. package/dist/types/native/components/BottomTabBar.d.ts +71 -1
  30. package/dist/types/native/components/Button.d.ts +10 -2
  31. package/dist/types/native/components/Celebration.d.ts +25 -0
  32. package/dist/types/native/components/CompactFilePicker.d.ts +27 -0
  33. package/dist/types/native/components/DetailSheetScaffold.d.ts +24 -0
  34. package/dist/types/native/components/GlassView.d.ts +33 -8
  35. package/dist/types/native/components/KeyboardAwareSheetFooter.d.ts +10 -0
  36. package/dist/types/native/components/MobileAppHeader.d.ts +13 -0
  37. package/dist/types/native/components/MobileAppShell.d.ts +15 -0
  38. package/dist/types/native/components/MobileFloatingActionButton.d.ts +15 -0
  39. package/dist/types/native/components/QuickActionGrid.d.ts +24 -0
  40. package/dist/types/native/components/SettingsSection.d.ts +25 -0
  41. package/dist/types/native/components/StatusActionBadge.d.ts +15 -0
  42. package/dist/types/native/components/index.d.ts +14 -3
  43. package/package.json +8 -3
  44. package/src/components/COMPONENT_LOOKUP.md +38 -6
@@ -0,0 +1,141 @@
1
+ # KSK Design System Native Recipes
2
+
3
+ Native / Expo consumer は、新規 UI を作る前に `ksk-design-system/native/ui` の既存コンポーネントを確認してください。ローカル `ds/` に独自 wrapper を増やす前に、このファイルの recipe を使います。
4
+
5
+ ## Expo Router / React Navigation bottom tabs
6
+
7
+ Expo Router の `<Tabs>` では `createExpoRouterTabBar` を渡します。アイコンは各 screen の `tabBarIcon` をそのまま使えるため、consumer 側で floating tab bar を組み直す必要はありません。
8
+
9
+ ```tsx
10
+ import { Tabs } from "expo-router"
11
+ import { createExpoRouterTabBar } from "ksk-design-system/native/ui"
12
+
13
+ const tabBar = createExpoRouterTabBar({
14
+ glass: true,
15
+ floating: true,
16
+ keyboardBehavior: "hide",
17
+ hiddenRouteNames: ["modal"],
18
+ })
19
+
20
+ export default function Layout() {
21
+ return (
22
+ <Tabs tabBar={tabBar}>
23
+ <Tabs.Screen name="index" options={{ title: "ホーム" }} />
24
+ <Tabs.Screen name="settings" options={{ title: "設定" }} />
25
+ <Tabs.Screen name="modal" options={{ href: null }} />
26
+ </Tabs>
27
+ )
28
+ }
29
+ ```
30
+
31
+ 個別画面で tab bar を隠す場合は `options={{ tabBarStyle: { display: "none" } }}` を使います。画面をルーティング対象から隠す場合は Expo Router の `href: null` を使います。
32
+
33
+ React Navigation でも同じ factory を `tabBar` に渡せます。
34
+
35
+ ```tsx
36
+ <Tab.Navigator tabBar={createExpoRouterTabBar({ glass: true, floating: true })}>
37
+ {/* screens */}
38
+ </Tab.Navigator>
39
+ ```
40
+
41
+ ## iOS 26 Liquid Glass
42
+
43
+ `GlassView` は `expo-glass-effect` が使える iOS 26 環境では native Liquid Glass を使い、未導入または非対応環境では `expo-blur`、最後に tokenized surface へフォールバックします。
44
+
45
+ ```bash
46
+ npx expo install expo-glass-effect expo-blur
47
+ ```
48
+
49
+ ```tsx
50
+ import { GlassView } from "ksk-design-system/native/ui"
51
+
52
+ <GlassView
53
+ nativeGlass
54
+ fallback="blur"
55
+ glassEffectStyle="regular"
56
+ tint="system"
57
+ intensity="regular"
58
+ borderRadius={28}
59
+ interactive
60
+ >
61
+ {/* tab bar, composer, floating action surface */}
62
+ </GlassView>
63
+ ```
64
+
65
+ `nativeGlass` を有効にしても Android / web / 非対応 iOS では自動で fallback します。feature check が必要な場合は `isNativeLiquidGlassAvailable()` を使います。
66
+
67
+ ## Button loading and icon slots
68
+
69
+ Native `Button` は `loading`、`loadingLabel`、`leadingIcon`、`trailingIcon` を持ちます。loading 中は disabled 扱いになり、`ActivityIndicator` は `Text` に包まれません。
70
+
71
+ ```tsx
72
+ <Button
73
+ variant="primary"
74
+ loading={saving}
75
+ loadingLabel="保存中"
76
+ leadingIcon={<SaveIcon />}
77
+ onPress={save}
78
+ >
79
+ 保存する
80
+ </Button>
81
+ ```
82
+
83
+ ## Mobile app shell
84
+
85
+ `MobileAppShell` は header / main / bottom nav / global FAB / desktop sidebar の基本 geometry を DS 側に寄せる recipe です。consumer 側で main padding や fixed nav の重なりを毎回計算しません。
86
+
87
+ ```tsx
88
+ import {
89
+ BottomTabBar,
90
+ MobileAppShell,
91
+ MobileFloatingActionButton,
92
+ } from "ksk-design-system/native/ui"
93
+
94
+ <MobileAppShell
95
+ header={<AppHeader title="Belle" />}
96
+ bottomNav={<BottomTabBar items={items} glass />}
97
+ fab={<MobileFloatingActionButton label="追加" onPress={openCreate} />}
98
+ >
99
+ <HomeScreen />
100
+ </MobileAppShell>
101
+ ```
102
+
103
+ Web/PWA consumer は `ksk-design-system` の `MobileAppShell` と `BottomTabBar variant="pill"` / `MobileFloatingActionButton` を組み合わせます。`bottomNavMode="fixed"` では shell が fixed wrapper と safe-area padding を持ちます。既に fixed な nav を渡す場合は `bottomNavMode="external"` を指定します。
104
+
105
+ ## Settings screens
106
+
107
+ 設定画面は `SettingsSection` と `SettingsListRow` を使います。`Card + SectionHeader + ListItem` のローカル wrapper を consumer 側で複製しないでください。
108
+
109
+ ```tsx
110
+ <SettingsSection title="通知" variant="card">
111
+ <SettingsListRow title="プッシュ通知" rightSlot={<Switch value={enabled} />} />
112
+ <SettingsListRow title="通知時間" description="毎日 9:00" rightSlot={<Badge>ON</Badge>} />
113
+ </SettingsSection>
114
+ ```
115
+
116
+ ## Attachments
117
+
118
+ Web では `CompactFilePicker` / `ImageAttachmentPicker` が hidden file input、trigger、preview、remove affordance を持ちます。Native では DocumentPicker / ImagePicker の起動だけ consumer が渡し、trigger と preview は DS が持ちます。
119
+
120
+ ```tsx
121
+ <ImageAttachmentPicker
122
+ multiple
123
+ images={images}
124
+ onFilesChange={setFiles}
125
+ onRemove={removeImage}
126
+ />
127
+ ```
128
+
129
+ ## Bottom sheet frames
130
+
131
+ `BottomSheetFrame` は `SheetContent` の外枠 preset です。中身は `DetailSheetScaffold` と `KeyboardAwareSheetFooter` をそのまま組み合わせます。
132
+
133
+ ```tsx
134
+ <Sheet open={open} onOpenChange={setOpen}>
135
+ <BottomSheetFrame preset="mobile-form">
136
+ <DetailSheetScaffold header={<DetailSheetHeader title="編集" />} footer={<KeyboardAwareSheetFooter />}>
137
+ {/* fields */}
138
+ </DetailSheetScaffold>
139
+ </BottomSheetFrame>
140
+ </Sheet>
141
+ ```
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **🔗 ライブ Storybook → https://ksk-design-system.vercel.app**
6
6
 
7
- 115 コンポーネントを実際に操作・確認できます。
7
+ 125 コンポーネントを実際に操作・確認できます。
8
8
 
9
9
  ---
10
10
 
@@ -14,7 +14,7 @@
14
14
  - **アクセシブル** — shadcn/ui(Radix UI ベース)+ `@storybook/addon-a11y` で a11y を担保
15
15
  - **Tailwind CSS v4 ネイティブ** — `@theme` ベースのトークン設計
16
16
  - **型安全** — React 19 + TypeScript、CVA によるバリアント管理
17
- - **115 + 91 コンポーネント** — Web 115(UI 56 / EC 11 / 管理 8 / シェル 3 / パターン 37)+ React Native 91
17
+ - **125 + 96 コンポーネント** — Web 125(UI 57 / EC 11 / 管理 8 / シェル 3 / パターン 46)+ React Native 96
18
18
  - **iOS 26 Liquid Glass 対応** — RN 側 `GlassView` + `Button variant="glass"`、Web 側 `.glass` CSS マテリアル
19
19
 
20
20
  ## 🎨 テーマ
@@ -54,6 +54,22 @@ import { Button, Card, Input, FormField } from "ksk-design-system"
54
54
 
55
55
  新規クライアント案件では、テーマファイルで `--Primitive-Brand-500` などブランドカラーの 10 行を定義するだけで、全コンポーネントがそのブランドカラーで動作します。
56
56
 
57
+ ### Consumer lint
58
+
59
+ consumer 側のローカル grep script が古くならないよう、DS 本体から `contracts/rules.json` を読む lint CLI を同梱しています。
60
+
61
+ ```bash
62
+ npx ksk-ds lint src
63
+ npx ksk-ds lint src --format json
64
+ npx ksk-ds lint --changed
65
+ ```
66
+
67
+ 出力は `file:line rule severity fix` を含みます。どうしても DS で表現できない domain-specific UI は、理由付きの escape コメントを置きます。
68
+
69
+ ```tsx
70
+ // ksk-ds-allow-custom-ui: medical chart requires bespoke interaction
71
+ ```
72
+
57
73
  ### Media overlay utilities
58
74
 
59
75
  動画・写真の上に文字や操作を置く場合は、`--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 をまとめて扱います。
@@ -92,6 +108,27 @@ import { Screen, PhotoHero, Button } from "ksk-design-system"
92
108
  </Screen>
93
109
  ```
94
110
 
111
+ ### Liquid Glass bottom navigation
112
+
113
+ Web のグローバルナビで iOS 26 風の Liquid Glass を使う場合は、`BottomTabBar variant="pill"` を使います。実アプリの中央 CTA は `centerAction`、ラベル付き構成は `showLabels`、暗い写真・動画・gradient 上では `tone="inverse"` を指定します。
114
+
115
+ ```tsx
116
+ import { BottomTabBar } from "ksk-design-system"
117
+
118
+ <BottomTabBar
119
+ variant="pill"
120
+ items={[
121
+ { label: "トーク", icon: <TalkIcon />, href: "/talk", isActive: true },
122
+ { label: "ギャラリー", icon: <GalleryIcon />, href: "/gallery" },
123
+ ]}
124
+ centerAction={{ label: "作成", icon: <PlusIcon />, href: "/create" }}
125
+ tone="inverse"
126
+ maxWidth={430}
127
+ />
128
+ ```
129
+
130
+ `pillPosition` は実アプリでは既定の `fixed`、Storybook や mobile shell 内のデモでは `absolute` を使います。safe-area は内部で `env(safe-area-inset-bottom)` を見ます。入力フォーム画面では keyboard 表示時に被らないよう、画面側で nav を隠すか bottom action に切り替えてください。
131
+
95
132
  ### React Native / Expo
96
133
 
97
134
  `ksk-design-system/native/ui` から直接 RN 用コンポーネント(91 個)を import できます。iOS 26 の **Liquid Glass** にも対応:
@@ -102,11 +139,13 @@ import { ThemeProvider, Button, Card, GlassView } from "ksk-design-system/native
102
139
 
103
140
  ```bash
104
141
  # Liquid Glass を本物の UIVisualEffectView で出したい場合
105
- npx expo install expo-blur
142
+ npx expo install expo-glass-effect expo-blur
106
143
  ```
107
144
 
108
145
  Web は backdrop-filter で擬似、Android は半透明 surface でフォールバックします。
109
146
 
147
+ Expo Router / React Navigation の tab bar、native `GlassView`、Button loading、settings/attachment/mobile shell recipes は `NATIVE_RECIPES.md` を参照してください。
148
+
110
149
  ## 🧪 試してみる(1コマンドお試し)
111
150
 
112
151
  ```bash
package/bin/init.js CHANGED
@@ -10,6 +10,7 @@
10
10
  // npx ksk-design-system init # AGENTS.md + CLAUDE.md を設置
11
11
  // npx ksk-design-system init --force # 既存ファイルを上書き
12
12
  // npx ksk-design-system demo [dir] # DS リポを clone + setup(お試し用)
13
+ // npx ksk-ds lint src # contracts/rules.json に基づき consumer UI を検査
13
14
  // npx ksk-design-system postinstall # npm postinstall から呼ばれる silent モード
14
15
 
15
16
  import { copyFileSync, existsSync } from "node:fs"
@@ -32,10 +33,19 @@ if (cmd === "help" || cmd === "--help" || cmd === "-h") {
32
33
  npx ksk-design-system init --force 既存ファイルを上書き
33
34
  npx ksk-design-system demo [dir] DS リポを clone + npm install(お試し)
34
35
  dir 省略時は ./ksk-ds-demo
36
+ npx ksk-ds lint src DS-first ルール違反を検査
37
+ npx ksk-ds lint src --format json CI 向け JSON 出力
38
+ npx ksk-ds lint --changed Git 差分のみ検査
35
39
  `)
36
40
  process.exit(0)
37
41
  }
38
42
 
43
+ if (cmd === "lint") {
44
+ const { runLintCli } = await import("./lint.js")
45
+ const status = await runLintCli(args.slice(1), { cwd: process.cwd(), pkgRoot })
46
+ process.exit(status)
47
+ }
48
+
39
49
  if (cmd === "demo") {
40
50
  runDemo(args.slice(1))
41
51
  process.exit(0)
package/bin/lint.js ADDED
@@ -0,0 +1,266 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"
2
+ import { extname, join, relative, resolve } from "node:path"
3
+ import { spawnSync } from "node:child_process"
4
+
5
+ const DEFAULT_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx"])
6
+ const DEFAULT_IGNORES = [
7
+ ".git",
8
+ ".next",
9
+ "build",
10
+ "coverage",
11
+ "dist",
12
+ "node_modules",
13
+ "storybook-static",
14
+ ]
15
+
16
+ export async function runLintCli(argv, { cwd = process.cwd(), pkgRoot = resolve(".") } = {}) {
17
+ const options = parseArgs(argv)
18
+ const rulesPath = resolve(pkgRoot, "contracts/rules.json")
19
+ if (!existsSync(rulesPath)) {
20
+ console.error(`contracts/rules.json が見つかりません: ${rulesPath}`)
21
+ return 1
22
+ }
23
+
24
+ const rules = loadRules(rulesPath)
25
+ const files = options.changed
26
+ ? getChangedFiles(cwd, options)
27
+ : collectTargetFiles(cwd, options.targets, options)
28
+ const findings = []
29
+
30
+ for (const file of files) {
31
+ findings.push(...lintFile(file, cwd, rules, options))
32
+ }
33
+
34
+ const summary = summarize(findings)
35
+ if (options.format === "json") {
36
+ console.log(JSON.stringify({ results: findings, summary }, null, 2))
37
+ } else {
38
+ printText(findings, summary)
39
+ }
40
+
41
+ return summary.errors > 0 ? 1 : 0
42
+ }
43
+
44
+ function parseArgs(argv) {
45
+ const targets = []
46
+ const excludes = []
47
+ let format = "text"
48
+ let changed = false
49
+
50
+ for (let i = 0; i < argv.length; i++) {
51
+ const arg = argv[i]
52
+ if (arg === "--format") {
53
+ format = argv[++i] ?? "text"
54
+ continue
55
+ }
56
+ if (arg.startsWith("--format=")) {
57
+ format = arg.slice("--format=".length)
58
+ continue
59
+ }
60
+ if (arg === "--changed") {
61
+ changed = true
62
+ continue
63
+ }
64
+ if (arg === "--exclude") {
65
+ excludes.push(argv[++i])
66
+ continue
67
+ }
68
+ if (arg.startsWith("--exclude=")) {
69
+ excludes.push(arg.slice("--exclude=".length))
70
+ continue
71
+ }
72
+ if (arg === "--help" || arg === "-h") {
73
+ console.log(`ksk-ds lint
74
+
75
+ 使い方:
76
+ ksk-ds lint [path ...]
77
+ ksk-ds lint src --changed
78
+ ksk-ds lint src --format json
79
+
80
+ オプション:
81
+ --changed Git の変更ファイルのみ検査
82
+ --format json JSON 出力
83
+ --exclude TEXT パスに TEXT を含むファイルを除外
84
+
85
+ 例外:
86
+ // ksk-ds-allow-custom-ui: domain-specific reason
87
+ `)
88
+ process.exit(0)
89
+ }
90
+ if (!arg.startsWith("-")) targets.push(arg)
91
+ }
92
+
93
+ return {
94
+ changed,
95
+ excludes: excludes.filter(Boolean),
96
+ format: format === "json" ? "json" : "text",
97
+ targets: targets.length > 0 ? targets : ["."],
98
+ }
99
+ }
100
+
101
+ function loadRules(rulesPath) {
102
+ const contract = JSON.parse(readFileSync(rulesPath, "utf8"))
103
+ const prohibited = Array.isArray(contract)
104
+ ? contract
105
+ : Array.isArray(contract.prohibited)
106
+ ? contract.prohibited
107
+ : []
108
+ const aiPatterns = Array.isArray(contract?.aiPatterns?.patterns)
109
+ ? contract.aiPatterns.patterns.map((rule) => ({ ...rule, category: "ai-pattern", severity: "warn" }))
110
+ : []
111
+
112
+ return [...prohibited, ...aiPatterns].filter((rule) => typeof rule.pattern === "string" && rule.pattern.length > 0)
113
+ }
114
+
115
+ function collectTargetFiles(cwd, targets, options) {
116
+ const files = []
117
+ for (const target of targets) {
118
+ const abs = resolve(cwd, target)
119
+ if (!existsSync(abs)) continue
120
+ collect(abs, cwd, options, files)
121
+ }
122
+ return files
123
+ }
124
+
125
+ function collect(abs, cwd, options, out) {
126
+ const rel = normalize(relative(cwd, abs))
127
+ if (shouldIgnorePath(rel, options)) return
128
+ const stat = statSync(abs)
129
+ if (stat.isDirectory()) {
130
+ for (const entry of readdirSync(abs)) {
131
+ collect(join(abs, entry), cwd, options, out)
132
+ }
133
+ return
134
+ }
135
+ if (stat.isFile() && DEFAULT_EXTENSIONS.has(extname(abs))) out.push(abs)
136
+ }
137
+
138
+ function getChangedFiles(cwd, options) {
139
+ const names = new Set()
140
+ for (const args of [
141
+ ["diff", "--name-only", "--diff-filter=ACMR", "origin/main...HEAD"],
142
+ ["diff", "--name-only", "--diff-filter=ACMR"],
143
+ ["diff", "--name-only", "--diff-filter=ACMR", "--cached"],
144
+ ]) {
145
+ const result = spawnSync("git", args, { cwd, encoding: "utf8" })
146
+ if (result.status !== 0) continue
147
+ for (const line of result.stdout.split(/\r?\n/)) {
148
+ if (line.trim()) names.add(line.trim())
149
+ }
150
+ }
151
+ return [...names]
152
+ .map((name) => resolve(cwd, name))
153
+ .filter((abs) => existsSync(abs))
154
+ .filter((abs) => !shouldIgnorePath(normalize(relative(cwd, abs)), options))
155
+ .filter((abs) => DEFAULT_EXTENSIONS.has(extname(abs)))
156
+ }
157
+
158
+ function lintFile(file, cwd, rules) {
159
+ const rel = normalize(relative(cwd, file))
160
+ const source = readFileSync(file, "utf8")
161
+ const escape = findEscape(source, rel)
162
+ if (escape.valid) return []
163
+
164
+ const findings = []
165
+ if (escape.invalid) findings.push(escape.invalid)
166
+ const lines = source.split(/\r?\n/)
167
+
168
+ for (const rule of rules) {
169
+ let regex
170
+ try {
171
+ regex = new RegExp(rule.pattern)
172
+ } catch {
173
+ continue
174
+ }
175
+ if (rule.pattern.includes("[\\s\\S]")) {
176
+ const match = source.match(regex)
177
+ if (match && match.index != null) {
178
+ const lineNumber = lineForIndex(source, match.index)
179
+ const line = lines[lineNumber - 1] ?? ""
180
+ if (!matchesRuleExclude(rule, rel, line)) {
181
+ findings.push(toFinding(rule, rel, lineNumber))
182
+ }
183
+ }
184
+ continue
185
+ }
186
+ for (let index = 0; index < lines.length; index++) {
187
+ const line = lines[index]
188
+ if (matchesRuleExclude(rule, rel, line)) continue
189
+ if (!regex.test(line)) continue
190
+ findings.push(toFinding(rule, rel, index + 1))
191
+ }
192
+ }
193
+
194
+ return findings
195
+ }
196
+
197
+ function toFinding(rule, file, line) {
198
+ return {
199
+ file,
200
+ line,
201
+ ruleId: rule.id ?? "UNKNOWN",
202
+ severity: rule.severity === "error" ? "error" : "warn",
203
+ category: rule.category ?? "pattern",
204
+ message: rule.message ?? rule.name ?? "DS rule violation",
205
+ fix: rule.fix ?? "",
206
+ }
207
+ }
208
+
209
+ function findEscape(source, file) {
210
+ const match = source.match(/ksk-ds-allow-custom-ui(?::\s*(.+))?/)
211
+ if (!match) return { valid: false }
212
+ const reason = match[1]?.trim()
213
+ if (reason) return { valid: true }
214
+ return {
215
+ valid: false,
216
+ invalid: {
217
+ file,
218
+ line: lineForIndex(source, match.index ?? 0),
219
+ ruleId: "ESCAPE001",
220
+ severity: "error",
221
+ category: "escape",
222
+ message: "ksk-ds-allow-custom-ui には理由が必要です",
223
+ fix: "例: // ksk-ds-allow-custom-ui: domain-specific reason",
224
+ },
225
+ }
226
+ }
227
+
228
+ function lineForIndex(source, index) {
229
+ return source.slice(0, index).split(/\r?\n/).length
230
+ }
231
+
232
+ function matchesRuleExclude(rule, file, line) {
233
+ const excludes = Array.isArray(rule.excludes) ? rule.excludes : []
234
+ return excludes.some((exclude) => file.includes(exclude) || line.includes(exclude))
235
+ }
236
+
237
+ function shouldIgnorePath(relPath, options) {
238
+ if (!relPath || relPath === ".") return false
239
+ const parts = relPath.split("/")
240
+ if (parts.some((part) => DEFAULT_IGNORES.includes(part))) return true
241
+ return options.excludes.some((exclude) => relPath.includes(exclude))
242
+ }
243
+
244
+ function summarize(findings) {
245
+ return {
246
+ files: new Set(findings.map((finding) => finding.file)).size,
247
+ errors: findings.filter((finding) => finding.severity === "error").length,
248
+ warnings: findings.filter((finding) => finding.severity !== "error").length,
249
+ }
250
+ }
251
+
252
+ function printText(findings, summary) {
253
+ if (findings.length === 0) {
254
+ console.log("ksk-ds lint: 違反は見つかりませんでした")
255
+ return
256
+ }
257
+ for (const finding of findings) {
258
+ console.log(`${finding.file}:${finding.line} ${finding.severity} ${finding.ruleId} ${finding.message}`)
259
+ if (finding.fix) console.log(` fix: ${finding.fix}`)
260
+ }
261
+ console.log(`\nksk-ds lint: ${summary.errors} error / ${summary.warnings} warn in ${summary.files} files`)
262
+ }
263
+
264
+ function normalize(path) {
265
+ return path.replaceAll("\\", "/")
266
+ }