@charcoal-ui/react 6.0.0-rc.2 → 6.0.0-rc.4
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/dist/components/Icon/index.d.ts +1 -1
- package/dist/components/Icon/index.d.ts.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +44 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/layered.css +44 -1
- package/dist/layered.css.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/css-variables.test.ts +1 -1
- package/src/__tests__/fixtures/legacy-v1-index.css +1836 -0
- package/src/__tests__/token-v1-compat.test.ts +118 -0
- package/src/__tests__/token-v1-remap.test.ts +133 -0
- package/src/components/Icon/index.tsx +2 -1
- package/src/components/Modal/__snapshots__/index.css.snap +1 -1
- package/src/components/Modal/index.css +1 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import postcss, { type Rule } from 'postcss'
|
|
5
|
+
|
|
6
|
+
// Design Token 1.0互換モードの「見た目の非破壊」を検証する。
|
|
7
|
+
//
|
|
8
|
+
// 移行: 旧 `@charcoal-ui/react/dist/index.css` (v1, --charcoal-* を直接使用)
|
|
9
|
+
// → 新 コンポーネントCSS (v2, --charcoal-color-* を使用) + `v1/remap.css` remap
|
|
10
|
+
//
|
|
11
|
+
// 非破壊であるためには、各 (セレクタ, プロパティ) で新CSSが使う 1つの --charcoal-color-*
|
|
12
|
+
// が、旧CSSで使われていた「単一の」--charcoal-* に対応していなければならない。同じ新トークンが
|
|
13
|
+
// 箇所ごとに異なる旧トークンに対応するなら、v2 が複数の v1 トークンを統合したということで、
|
|
14
|
+
// remap がどんな値を選んでも一部の箇所で色が変わる = 見た目が壊れる。
|
|
15
|
+
// 例: --charcoal-color-container-neutral-default は Modal 背景 (旧 surface4 = 暗幕) と
|
|
16
|
+
// Switch (旧 text4) と Checkbox (旧 text3) に使われ、単一値では再現不可。
|
|
17
|
+
//
|
|
18
|
+
// 旧 index.css は charcoal の直前リリース (published v1) を fixtures に取り込んだもの。
|
|
19
|
+
|
|
20
|
+
const legacyPath = path.resolve(
|
|
21
|
+
import.meta.dirname,
|
|
22
|
+
'fixtures/legacy-v1-index.css',
|
|
23
|
+
)
|
|
24
|
+
const componentsDir = path.resolve(import.meta.dirname, '../components')
|
|
25
|
+
|
|
26
|
+
async function getCssFiles(dir: string): Promise<string[]> {
|
|
27
|
+
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
28
|
+
const results = await Promise.all(
|
|
29
|
+
entries.map(async (entry) => {
|
|
30
|
+
const entryPath = path.join(dir, entry.name)
|
|
31
|
+
if (entry.isDirectory()) return getCssFiles(entryPath)
|
|
32
|
+
if (entry.isFile() && entry.name.endsWith('.css')) return [entryPath]
|
|
33
|
+
return []
|
|
34
|
+
}),
|
|
35
|
+
)
|
|
36
|
+
return results.flat()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 親 Rule を辿ってネストしたセレクタパスを組み立てる。 */
|
|
40
|
+
function selectorPath(rule: Rule): string {
|
|
41
|
+
const parts: string[] = []
|
|
42
|
+
let node: postcss.Container | postcss.Document | undefined = rule
|
|
43
|
+
while (node && node.type === 'rule') {
|
|
44
|
+
parts.unshift((node as Rule).selector.replace(/\s+/g, ' ').trim())
|
|
45
|
+
node = node.parent as postcss.Container | undefined
|
|
46
|
+
}
|
|
47
|
+
return parts.join(' >> ')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `${selectorPath} | ${prop}` -> 単一 var(--charcoal…) の参照トークン。 */
|
|
51
|
+
function declarationTokens(css: string): Map<string, string> {
|
|
52
|
+
const root = postcss.parse(css)
|
|
53
|
+
const map = new Map<string, string>()
|
|
54
|
+
root.walkDecls((decl) => {
|
|
55
|
+
const m = decl.value.match(/^var\(\s*(--charcoal-[a-z0-9-]+)\s*\)$/)
|
|
56
|
+
if (!m || decl.parent?.type !== 'rule') return
|
|
57
|
+
map.set(`${selectorPath(decl.parent as Rule)} | ${decl.prop}`, m[1])
|
|
58
|
+
})
|
|
59
|
+
return map
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const legacy = declarationTokens(await fs.readFile(legacyPath, 'utf-8'))
|
|
63
|
+
const modern = new Map<string, string>()
|
|
64
|
+
for (const file of await getCssFiles(componentsDir)) {
|
|
65
|
+
for (const [key, token] of declarationTokens(
|
|
66
|
+
await fs.readFile(file, 'utf-8'),
|
|
67
|
+
)) {
|
|
68
|
+
modern.set(key, token)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 新旧で一致する (セレクタ, プロパティ) について 新トークン -> {旧トークン…}
|
|
73
|
+
const correspondence = new Map<string, Map<string, string>>()
|
|
74
|
+
for (const [key, newToken] of modern) {
|
|
75
|
+
if (!legacy.has(key)) continue
|
|
76
|
+
if (!newToken.startsWith('--charcoal-color-')) continue
|
|
77
|
+
if (!correspondence.has(newToken)) correspondence.set(newToken, new Map())
|
|
78
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
79
|
+
correspondence.get(newToken)!.set(key, legacy.get(key)!)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('Design Token 1.0互換: 旧 index.css の見た目を壊さない', () => {
|
|
83
|
+
it('新トークンは旧の単一 --charcoal-* に対応する (v2 でのトークン統合による破壊がない)', () => {
|
|
84
|
+
const breaking = [...correspondence]
|
|
85
|
+
.map(([newToken, byKey]) => ({
|
|
86
|
+
newToken,
|
|
87
|
+
oldTokens: [...new Set(byKey.values())].sort(),
|
|
88
|
+
usages: [...byKey.entries()].map(([k, v]) => `${k} → ${v}`),
|
|
89
|
+
}))
|
|
90
|
+
.filter(({ oldTokens }) => oldTokens.length > 1)
|
|
91
|
+
.map(
|
|
92
|
+
({ newToken, oldTokens, usages }, index) =>
|
|
93
|
+
`${index + 1}) ${newToken} は旧の {${oldTokens.join(', ')}} を破壊的に統合:\n - ${usages.join('\n - ')}`,
|
|
94
|
+
)
|
|
95
|
+
.join('\n')
|
|
96
|
+
|
|
97
|
+
expect(breaking).toMatchInlineSnapshot(`
|
|
98
|
+
"1) --charcoal-color-text-default は旧の {--charcoal-text1, --charcoal-text2} を破壊的に統合:
|
|
99
|
+
- .charcoal-button | color → --charcoal-text2
|
|
100
|
+
- .charcoal-checkbox__label_div | color → --charcoal-text2
|
|
101
|
+
- .charcoal-ui-dropdown-selector-text | color → --charcoal-text2
|
|
102
|
+
- .charcoal-field-label | color → --charcoal-text1
|
|
103
|
+
- .charcoal-modal-header-title | color → --charcoal-text1
|
|
104
|
+
- .charcoal-multi-select-label | color → --charcoal-text2
|
|
105
|
+
- .charcoal-radio__label_div | color → --charcoal-text2
|
|
106
|
+
- .charcoal-switch__label_div | color → --charcoal-text2
|
|
107
|
+
2) --charcoal-color-border-default は旧の {--charcoal-text3, --charcoal-text4} を破壊的に統合:
|
|
108
|
+
- .charcoal-checkbox-input:not(:checked) | border-color → --charcoal-text4
|
|
109
|
+
- .charcoal-radio-input:not(:checked) | border-color → --charcoal-text3
|
|
110
|
+
3) --charcoal-color-icon-tertiary-default は旧の {--charcoal-text3, --charcoal-text4} を破壊的に統合:
|
|
111
|
+
- .charcoal-hint-text-icon | color → --charcoal-text3
|
|
112
|
+
- .charcoal-loading-spinner | color → --charcoal-text4
|
|
113
|
+
4) --charcoal-color-container-neutral-default は旧の {--charcoal-text3, --charcoal-text4} を破壊的に統合:
|
|
114
|
+
- .charcoal-multi-select-input[type='checkbox'] | background-color → --charcoal-text3
|
|
115
|
+
- .charcoal-switch-input | background-color → --charcoal-text4"
|
|
116
|
+
`)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import postcss from 'postcss'
|
|
5
|
+
|
|
6
|
+
// Design Token 1.0互換モード (`@charcoal-ui/theme/css/v1/remap.css`) の remap を検証する。
|
|
7
|
+
//
|
|
8
|
+
// アプリは 1.0互換 (`v1/remap.css`) か 2.0 (`v2/{light,dark}.css`) のいずれかで --charcoal-color-*
|
|
9
|
+
// を供給し、コンポーネント実装CSS (`@charcoal-ui/react`) がそれを消費する。remap が
|
|
10
|
+
// 「壊さない・冗長でない・正しい」ことを次で担保する:
|
|
11
|
+
// 1. 非破壊: コンポーネントが使う --charcoal-color-* は全て remap 経由で解決できる。
|
|
12
|
+
// 2. 正しさ: --charcoal-color-* 同士の参照は全て定義済みで循環しない。重複定義がない。
|
|
13
|
+
// 3. 冗長でない: 2.0 API にも無く・内部参照も無く・コンポーネントも使わないトークンを持たない。
|
|
14
|
+
|
|
15
|
+
const cssDir = path.resolve(import.meta.dirname, '../../../theme/src/css')
|
|
16
|
+
const tokenV1Path = path.join(cssDir, 'v1/remap.css')
|
|
17
|
+
const variables2Path = path.join(cssDir, 'v2/light.css')
|
|
18
|
+
const componentsDir = path.resolve(import.meta.dirname, '../components')
|
|
19
|
+
|
|
20
|
+
const COLOR_PREFIX = '--charcoal-color-'
|
|
21
|
+
const colorVarsIn = (value: string): string[] =>
|
|
22
|
+
[...value.matchAll(/var\(\s*(--charcoal-color-[a-z0-9-]+)/g)].map((m) => m[1])
|
|
23
|
+
|
|
24
|
+
async function getCssFiles(dir: string): Promise<string[]> {
|
|
25
|
+
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
26
|
+
const results = await Promise.all(
|
|
27
|
+
entries.map(async (entry) => {
|
|
28
|
+
const entryPath = path.join(dir, entry.name)
|
|
29
|
+
if (entry.isDirectory()) return getCssFiles(entryPath)
|
|
30
|
+
if (entry.isFile() && entry.name.endsWith('.css')) return [entryPath]
|
|
31
|
+
return []
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
34
|
+
return results.flat()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** --charcoal-color-* の定義 (prop -> value) を収集。重複も検出。 */
|
|
38
|
+
function parseColorDefinitions(css: string): {
|
|
39
|
+
defs: Map<string, string>
|
|
40
|
+
duplicates: string[]
|
|
41
|
+
} {
|
|
42
|
+
const defs = new Map<string, string>()
|
|
43
|
+
const duplicates: string[] = []
|
|
44
|
+
postcss.parse(css).walkDecls((decl) => {
|
|
45
|
+
if (!decl.prop.startsWith(COLOR_PREFIX)) return
|
|
46
|
+
if (defs.has(decl.prop)) duplicates.push(decl.prop)
|
|
47
|
+
defs.set(decl.prop, decl.value)
|
|
48
|
+
})
|
|
49
|
+
return { defs, duplicates }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 宣言値から使用中の --charcoal-color-* を収集。 */
|
|
53
|
+
function usedColorVars(css: string): Set<string> {
|
|
54
|
+
const used = new Set<string>()
|
|
55
|
+
postcss.parse(css).walkDecls((decl) => {
|
|
56
|
+
for (const v of colorVarsIn(decl.value)) used.add(v)
|
|
57
|
+
})
|
|
58
|
+
return used
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** --charcoal-color-* の var() チェインを解決。未定義参照・循環があれば失敗。 */
|
|
62
|
+
function resolve(
|
|
63
|
+
token: string,
|
|
64
|
+
defs: Map<string, string>,
|
|
65
|
+
seen = new Set<string>(),
|
|
66
|
+
): { ok: true } | { ok: false; reason: string } {
|
|
67
|
+
if (!defs.has(token)) return { ok: false, reason: `undefined ${token}` }
|
|
68
|
+
if (seen.has(token)) return { ok: false, reason: `cycle at ${token}` }
|
|
69
|
+
seen.add(token)
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
71
|
+
for (const ref of colorVarsIn(defs.get(token)!)) {
|
|
72
|
+
const r = resolve(ref, defs, seen)
|
|
73
|
+
if (!r.ok) return r
|
|
74
|
+
}
|
|
75
|
+
return { ok: true } // --charcoal-color-* を含まない = 終端 (--charcoal-* かリテラル)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const { defs: tokenDefs, duplicates } = parseColorDefinitions(
|
|
79
|
+
await fs.readFile(tokenV1Path, 'utf-8'),
|
|
80
|
+
)
|
|
81
|
+
const variables2Defs = parseColorDefinitions(
|
|
82
|
+
await fs.readFile(variables2Path, 'utf-8'),
|
|
83
|
+
).defs
|
|
84
|
+
|
|
85
|
+
const componentCssFiles = await getCssFiles(componentsDir)
|
|
86
|
+
const componentUsed = new Set<string>()
|
|
87
|
+
for (const file of componentCssFiles) {
|
|
88
|
+
for (const v of usedColorVars(await fs.readFile(file, 'utf-8')))
|
|
89
|
+
componentUsed.add(v)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe('Design Token 1.0互換 (v1/remap.css) remap', () => {
|
|
93
|
+
it('重複定義がない', () => {
|
|
94
|
+
expect(duplicates).toEqual([])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('全ての --charcoal-color-* が解決する (dangling / 循環がない)', () => {
|
|
98
|
+
const broken = [...tokenDefs.keys()]
|
|
99
|
+
.map((token) => ({ token, r: resolve(token, tokenDefs) }))
|
|
100
|
+
.filter(({ r }) => !r.ok)
|
|
101
|
+
.map(({ token, r }) => `${token}: ${(r as { reason: string }).reason}`)
|
|
102
|
+
|
|
103
|
+
expect(broken).toEqual([])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('冗長トークンがない (2.0 API にも無く・内部参照も無く・コンポーネント未使用)', () => {
|
|
107
|
+
const internallyReferenced = new Set<string>()
|
|
108
|
+
for (const value of tokenDefs.values()) {
|
|
109
|
+
for (const ref of colorVarsIn(value)) internallyReferenced.add(ref)
|
|
110
|
+
}
|
|
111
|
+
const redundant = [...tokenDefs.keys()].filter(
|
|
112
|
+
(t) =>
|
|
113
|
+
!variables2Defs.has(t) &&
|
|
114
|
+
!internallyReferenced.has(t) &&
|
|
115
|
+
!componentUsed.has(t),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
expect(redundant).toEqual([])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it.each(componentCssFiles)(
|
|
122
|
+
'%s: 使用する --charcoal-color-* は全て v1/remap.css 経由で解決する (非破壊)',
|
|
123
|
+
async (file) => {
|
|
124
|
+
const css = await fs.readFile(file, 'utf-8')
|
|
125
|
+
const unresolved = [...usedColorVars(css)]
|
|
126
|
+
.map((token) => ({ token, r: resolve(token, tokenDefs) }))
|
|
127
|
+
.filter(({ r }) => !r.ok)
|
|
128
|
+
.map(({ token }) => token)
|
|
129
|
+
|
|
130
|
+
expect(unresolved).toEqual([])
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
})
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
|
|
3
3
|
import '@charcoal-ui/icons'
|
|
4
|
-
|
|
4
|
+
// tmp fix for https://github.com/rolldown/tsdown/pull/981
|
|
5
|
+
import '../../../../icons/css/icon.css'
|
|
5
6
|
import { calcActualSize } from '@charcoal-ui/icons'
|
|
6
7
|
import type { IconSizing, PixivIcon, Props } from '@charcoal-ui/icons'
|
|
7
8
|
|