ksk-design-system 1.66.0 → 1.66.2

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
@@ -229,6 +229,22 @@ consumer が shadcn 既定の `src/components/ui/` に部品を置いても中
229
229
  (v1.63.x までは単一の `excludes` をパスと行内容の両方に OR で当てていたため、
230
230
  このディレクトリ名だけで 13 ルールが同時に黙っていました)。
231
231
 
232
+ `excludeLines` の各値は **literal 文字列**として `line.includes(value)` で判定されます
233
+ (正規表現ではありません)。`shadow-\[` のように正規表現エスケープした値を書くと、実ソースの
234
+ `shadow-[var(--shadow-md)]` と一致せず除外が一度も効きません(issue #463)。
235
+ `scripts/check-rules-contract.mjs`(`npm run check` に組み込み済み)が `excludeLines` /
236
+ `excludePaths` / `excludeDsPaths` 内のバックスラッシュ混入を検出します。
237
+
238
+ #### P029: テンプレートリテラル className の許容範囲(issue #464)
239
+
240
+ `className={\`...\${...}...\`}` の補間部(`${...}`)が**文字列リテラルのみで構成される式**
241
+ (単純三項 `cond ? "a" : "b"` / `&&` / ネスト三項)なら、Tailwind の静的クラス抽出を壊さないため
242
+ P029 の対象外です。CLAUDE.md の実装前セルフチェックにある「クラス名は完全な文字列で書く。分岐は
243
+ 三項演算子か cva variant で」という方針と一致させています。
244
+
245
+ 識別子・関数呼び出し・メンバーアクセスの補間(`` `bg-${color}` `` 等)や、テンプレートリテラルの
246
+ 入れ子は静的抽出できないため引き続き検出します。`cn()` / `clsx()` / CVA を使ってください。
247
+
232
248
  ### Jest(CommonJS)でコンポーネントをテストする
233
249
 
234
250
  このパッケージは **ESM-only** です。CJS との dual build は配布せず、Jest
package/bin/lint.js CHANGED
@@ -789,6 +789,7 @@ function lintFile(file, cwd, rules, options = {}) {
789
789
  if (matchesRuleExclude(rule, { file: rel, line: rawLine, isDsFile })) continue
790
790
  const maskedLine = maskedLines[index] ?? ""
791
791
  if (!regex.test(maskedLine)) continue
792
+ if (rule.id === "P029" && isP029TemplateLiteralExempt(rawLine)) continue
792
793
  if (ignores.suppresses(rule.id ?? "UNKNOWN", index + 1)) continue
793
794
  findings.push(toFinding(rule, rel, index + 1, platform))
794
795
  }
@@ -1119,6 +1120,188 @@ function maskTemplateLiterals(source) {
1119
1120
  return maskStrings(source, TEMPLATE_DELIMITER_ONLY)
1120
1121
  }
1121
1122
 
1123
+ /**
1124
+ * P029(テンプレートリテラル className 禁止)の例外判定(issue #464)。
1125
+ *
1126
+ * `className={\`base ${cond ? "a" : "b"}\`}` のように、補間部(`${...}`)が
1127
+ * 「文字列リテラルのみで構成される式」(三項・&&・ネスト三項を含む)なら
1128
+ * Tailwind の静的クラス抽出を壊さないため対象外にする。`${color}` のような
1129
+ * 識別子・関数呼び出し・メンバーアクセスの補間は従来どおり検出する。
1130
+ *
1131
+ * 文字列一致ベースの簡易パーサーであり、TypeScript の完全な式パーサーではない。
1132
+ * 判定に迷うケース(バッククォートのネスト等)は安全側=「例外にしない」を返す。
1133
+ */
1134
+
1135
+ /** 文字列/括弧の深さを見ながら、depth 0 の `?` `:` `&&` `||` の位置を集める */
1136
+ function scanTopLevelOperatorTokens(expr) {
1137
+ const tokens = []
1138
+ let depth = 0
1139
+ let quote = null
1140
+ for (let i = 0; i < expr.length; i++) {
1141
+ const ch = expr[i]
1142
+ if (quote) {
1143
+ if (ch === "\\") {
1144
+ i += 1
1145
+ continue
1146
+ }
1147
+ if (ch === quote) quote = null
1148
+ continue
1149
+ }
1150
+ if (ch === '"' || ch === "'") {
1151
+ quote = ch
1152
+ continue
1153
+ }
1154
+ if (ch === "(" || ch === "[" || ch === "{") {
1155
+ depth += 1
1156
+ continue
1157
+ }
1158
+ if (ch === ")" || ch === "]" || ch === "}") {
1159
+ depth -= 1
1160
+ continue
1161
+ }
1162
+ if (depth !== 0) continue
1163
+ if (ch === "?") tokens.push({ index: i, type: "?" })
1164
+ else if (ch === ":") tokens.push({ index: i, type: ":" })
1165
+ else if (ch === "&" && expr[i + 1] === "&") {
1166
+ tokens.push({ index: i, type: "&&" })
1167
+ i += 1
1168
+ } else if (ch === "|" && expr[i + 1] === "|") {
1169
+ tokens.push({ index: i, type: "||" })
1170
+ i += 1
1171
+ }
1172
+ }
1173
+ return tokens
1174
+ }
1175
+
1176
+ /** `cond ? a : b` を depth 0 の `?`/`:` のバランスで分割する(ネスト三項対応) */
1177
+ function splitTopLevelTernary(expr) {
1178
+ const tokens = scanTopLevelOperatorTokens(expr)
1179
+ const questionIndex = tokens.findIndex((token) => token.type === "?")
1180
+ if (questionIndex === -1) return null
1181
+ let balance = 1
1182
+ for (let i = questionIndex + 1; i < tokens.length; i++) {
1183
+ if (tokens[i].type === "?") balance += 1
1184
+ else if (tokens[i].type === ":") {
1185
+ balance -= 1
1186
+ if (balance === 0) {
1187
+ const questionPos = tokens[questionIndex].index
1188
+ const colonPos = tokens[i].index
1189
+ return {
1190
+ whenTrue: expr.slice(questionPos + 1, colonPos),
1191
+ whenFalse: expr.slice(colonPos + 1),
1192
+ }
1193
+ }
1194
+ }
1195
+ }
1196
+ return null
1197
+ }
1198
+
1199
+ /** `lhs && rhs` / `lhs || rhs` を最初の depth 0 演算子で分割する */
1200
+ function splitTopLevelLogical(expr) {
1201
+ const token = scanTopLevelOperatorTokens(expr).find(
1202
+ (candidate) => candidate.type === "&&" || candidate.type === "||",
1203
+ )
1204
+ if (!token) return null
1205
+ return { rhs: expr.slice(token.index + token.type.length) }
1206
+ }
1207
+
1208
+ function isStringLiteralExpr(expr) {
1209
+ const trimmed = expr.trim()
1210
+ return /^"(?:[^"\\]|\\.)*"$/.test(trimmed) || /^'(?:[^'\\]|\\.)*'$/.test(trimmed)
1211
+ }
1212
+
1213
+ /** `(expr)` のように括弧が式全体をちょうど一度だけ包んでいるか */
1214
+ function isFullyParenWrapped(expr) {
1215
+ if (expr[0] !== "(" || expr[expr.length - 1] !== ")") return false
1216
+ let depth = 0
1217
+ for (let i = 0; i < expr.length; i++) {
1218
+ if (expr[i] === "(") depth += 1
1219
+ else if (expr[i] === ")") {
1220
+ depth -= 1
1221
+ if (depth === 0) return i === expr.length - 1
1222
+ }
1223
+ }
1224
+ return false
1225
+ }
1226
+
1227
+ /** 補間式が「文字列リテラルのみで構成される式」か(issue #464) */
1228
+ export function isLiteralOnlyInterpolation(expr) {
1229
+ const trimmed = (expr ?? "").trim()
1230
+ if (trimmed === "") return false
1231
+ // ネストしたテンプレートリテラルは判定を諦めて安全側(=対象外にしない)に倒す
1232
+ if (trimmed.includes("`")) return false
1233
+ if (isStringLiteralExpr(trimmed)) return true
1234
+ if (isFullyParenWrapped(trimmed)) return isLiteralOnlyInterpolation(trimmed.slice(1, -1))
1235
+ const ternary = splitTopLevelTernary(trimmed)
1236
+ if (ternary) {
1237
+ return isLiteralOnlyInterpolation(ternary.whenTrue) && isLiteralOnlyInterpolation(ternary.whenFalse)
1238
+ }
1239
+ const logical = splitTopLevelLogical(trimmed)
1240
+ if (logical) return isLiteralOnlyInterpolation(logical.rhs)
1241
+ return false
1242
+ }
1243
+
1244
+ /** テンプレートリテラル(バッククォート開始位置つき)の中身を取り出す。同一行内で閉じない場合は null */
1245
+ function extractTemplateLiteralBody(line, backtickIndex) {
1246
+ let i = backtickIndex + 1
1247
+ let braceDepth = 0
1248
+ let content = ""
1249
+ while (i < line.length) {
1250
+ const ch = line[i]
1251
+ if (ch === "\\") {
1252
+ content += ch + (line[i + 1] ?? "")
1253
+ i += 2
1254
+ continue
1255
+ }
1256
+ if (braceDepth === 0 && ch === "`") return content
1257
+ if (ch === "{") braceDepth += 1
1258
+ else if (ch === "}" && braceDepth > 0) braceDepth -= 1
1259
+ content += ch
1260
+ i += 1
1261
+ }
1262
+ return null
1263
+ }
1264
+
1265
+ /** テンプレートリテラルの中身から `${...}` の中身だけを depth バランスで取り出す */
1266
+ function extractInterpolationExprs(content) {
1267
+ const exprs = []
1268
+ let i = content.indexOf("${")
1269
+ while (i !== -1) {
1270
+ const start = i + 2
1271
+ let depth = 1
1272
+ let j = start
1273
+ while (j < content.length && depth > 0) {
1274
+ if (content[j] === "{") depth += 1
1275
+ else if (content[j] === "}") depth -= 1
1276
+ j += 1
1277
+ }
1278
+ if (depth === 0) {
1279
+ exprs.push(content.slice(start, j - 1))
1280
+ i = content.indexOf("${", j)
1281
+ } else {
1282
+ exprs.push(content.slice(start))
1283
+ break
1284
+ }
1285
+ }
1286
+ return exprs
1287
+ }
1288
+
1289
+ /**
1290
+ * P029 が fire した行が「className の template literal で、補間部が全て
1291
+ * 文字列リテラルのみで構成される式」なら true(=対象外にしてよい)を返す。
1292
+ */
1293
+ export function isP029TemplateLiteralExempt(line) {
1294
+ const classNameIndex = line.indexOf("className=")
1295
+ if (classNameIndex === -1) return false
1296
+ const backtickIndex = line.indexOf("`", classNameIndex)
1297
+ if (backtickIndex === -1) return false
1298
+ const content = extractTemplateLiteralBody(line, backtickIndex)
1299
+ if (content === null) return false
1300
+ const interpolations = extractInterpolationExprs(content)
1301
+ if (interpolations.length === 0) return false
1302
+ return interpolations.every((expr) => isLiteralOnlyInterpolation(expr))
1303
+ }
1304
+
1122
1305
  function excludeList(value) {
1123
1306
  return Array.isArray(value) ? value : []
1124
1307
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
3
  "name": "KSK Design System — Component Contracts",
4
- "version": "1.66.0",
4
+ "version": "1.66.2",
5
5
  "description": "全コンポーネントの構造化定義。バリアント・アクセシビリティ要件・使用ルールを機械可読形式で管理。",
6
6
  "counts": {
7
7
  "ui": 68,
@@ -228,7 +228,7 @@
228
228
  "pattern": "shadow-md\\b|shadow-lg\\b|shadow-xl\\b|shadow-2xl\\b",
229
229
  "excludePaths": [".stories."],
230
230
  "excludeDsPaths": ["components/ui/", "components/patterns/"],
231
- "excludeLines": ["shadow-\\["],
231
+ "excludeLines": ["shadow-["],
232
232
  "message": "DS 定義外のシャドウ禁止",
233
233
  "fix": "shadow-[var(--shadow-sm/md/lg/dialog/tooltip)]"
234
234
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
3
  "name": "KSK Design System — Semantic Token Hex Cache",
4
- "version": "1.66.0",
4
+ "version": "1.66.2",
5
5
  "description": "semantic / semanticDark トークン(var(--Primitive-*) 参照)を実 hex に解決したサイドカー生成物。hex はデフォルト(Blue)テーマでの解決値であり、Brand 系(meta.themeDependentKeys に列挙)はテーマ差し替え(orange/green/violet 等)で実色が変わる。テーマ別の完全解決値は `ksk-design-system/native` エクスポート(バンドル済み native トークンモジュール)の themes を参照。tokens.json 本体のスキーマは変更せず、AI がこのファイルだけで実色を把握できるようにし、primitive 値の変更による semantic 実色のドリフトを --check で機械検出する。",
6
6
  "generatedBy": "scripts/generate-token-hex-cache.mjs",
7
7
  "theme": "default",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ksk-design-system",
3
- "version": "1.66.0",
3
+ "version": "1.66.2",
4
4
  "type": "module",
5
5
  "description": "KSK Design System — フリーランス向けマルチテーマ対応デザインシステム",
6
6
  "license": "MIT",
@@ -92,6 +92,10 @@
92
92
  "import": "./src/styles/categorical.css",
93
93
  "default": "./src/styles/categorical.css"
94
94
  },
95
+ "./tokens/product-theme": {
96
+ "import": "./src/styles/product-theme.css",
97
+ "default": "./src/styles/product-theme.css"
98
+ },
95
99
  "./glass": {
96
100
  "import": "./src/styles/glass.css",
97
101
  "default": "./src/styles/glass.css"
@@ -13,6 +13,11 @@
13
13
  @import "./themes/my-client.css"; … Brand 10行(色)
14
14
  @import "./product-theme.css"; … ここの変数(寸法・面)
15
15
 
16
+ preset を使わずトークン CSS を個別 import する消費側は、このファイルを
17
+ subpath export で直接読める(コピー同期は不要):
18
+
19
+ @import "ksk-design-system/tokens/product-theme";
20
+
16
21
  機械可読な許可リストは contracts/product-theme-overrides.json が正本。
17
22
  **ここに無い変数は公開契約ではない**(内部実装なので予告なく変わる)。
18
23
  許可リスト外の上書きは `npx ksk-ds lint` の P049 が検出する。