@charcoal-ui/icons 6.0.0-beta.0 → 6.0.0-beta.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 +6 -5
- package/css/icon.css +44 -0
- package/css/v1/index.css +10 -0
- package/css/v1/index.html +9 -0
- package/css/v1/index.story.tsx +10 -1
- package/dist/index.cjs +58 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +34 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/react/v1/16/Description.d.ts +4 -0
- package/react/v1/16/Description.d.ts.map +1 -0
- package/react/v1/16/Description.js +5 -0
- package/react/v1/index.d.ts +1 -0
- package/react/v1/index.d.ts.map +1 -1
- package/react/v1/index.js +1 -0
- package/src/PixivIcon.story.tsx +33 -2
- package/src/PixivIcon.test.tsx +215 -52
- package/src/PixivIcon.ts +38 -55
- package/src/README.mdx +7 -2
- package/src/SSR.mdx +127 -43
- package/src/calcActualSize.ts +115 -0
- package/src/index.ts +1 -0
- package/src/react/v1/16/Description.tsx +28 -0
- package/src/react/v1/index.story.tsx +6 -0
- package/src/react/v1/index.tsx +1 -0
package/src/SSR.mdx
CHANGED
|
@@ -8,58 +8,142 @@ import { Meta, Story, Canvas } from '@storybook/addon-docs/blocks'
|
|
|
8
8
|
`@charcoal-ui/icons` は、Node.js の環境下で import されたり、API を呼び出されても問題が起きないように設計されています。
|
|
9
9
|
|
|
10
10
|
一方、Custom Element であるということは、**SVG アイコンの読み込みはクライアントサイドで行われます**。
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
&[name~='32/'] {
|
|
32
|
-
--icon-size: 32px;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
&[scale='2'] {
|
|
36
|
-
--scale: 2;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
&[scale='3'] {
|
|
40
|
-
--scale: 3;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/* NOTICE: 現状とサポートブラウザが少ない */
|
|
44
|
-
@supports (--scale: attr(unsafe-non-guideline-scale)) {
|
|
45
|
-
&[unsafe-non-guideline-scale] {
|
|
46
|
-
--scale: attr(unsafe-non-guideline-scale);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
11
|
+
したがって、大きさが確定せずにレイアウトシフト (CLS) が起こりうるのは、SSR における注意点の一つと言えます。
|
|
12
|
+
|
|
13
|
+
## なぜ CSS が必要なのか
|
|
14
|
+
|
|
15
|
+
`@charcoal-ui/icons` の JS が読み込まれて `customElements.define('pixiv-icon', ...)` が実行されるまで、`<pixiv-icon>` は Shadow DOM を持たない単なる未登録 Custom Element です。CSS がない場合、ブラウザはこの要素のサイズを 0x0 として扱うため:
|
|
16
|
+
|
|
17
|
+
- SSR で送出された HTML が画面に描画された直後: アイコンは **0x0** で何も見えない
|
|
18
|
+
- JS がロードされて `pixiv-icon` が `:defined` になった瞬間: 突然 16x16 / 24x24 などの実サイズになり、**周囲のコンテンツが押し出される**
|
|
19
|
+
|
|
20
|
+
この瞬間に発生する CLS を、`icon.css` が事前に寸法を確保することで防ぎます。
|
|
21
|
+
|
|
22
|
+
## レイアウトシフト防止 CSS
|
|
23
|
+
|
|
24
|
+
`@charcoal-ui/icons/css/icon.css` をインポートすると、`<pixiv-icon>` のレイアウトシフトを防止できます。
|
|
25
|
+
アプリケーションのエントリポイント、または Storybook の `preview.(js|ts)` で 1 度だけ読み込んでください。
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import '@charcoal-ui/icons/css/icon.css'
|
|
49
29
|
```
|
|
50
30
|
|
|
51
|
-
|
|
52
|
-
|
|
31
|
+
この CSS は 2 つのメカニズムを提供します。
|
|
32
|
+
|
|
33
|
+
### 1. `.charcoal-icon` クラス(React 向け)
|
|
34
|
+
|
|
35
|
+
`@charcoal-ui/react` の `<Icon>` コンポーネントは自動的に `.charcoal-icon` クラスと `--charcoal-icon-size` CSS 変数を設定します。
|
|
36
|
+
CSS が読み込まれていれば、Custom Element の定義前でも正しいサイズが確保されます。
|
|
37
|
+
|
|
38
|
+
### 2. `pixiv-icon:not(:defined)` セレクター(vanilla HTML 向け)
|
|
39
|
+
|
|
40
|
+
React を使わない場合でも、`name` 属性のプレフィックス(`16/`, `24/`, `32/`, `Inline/`)と `scale` 属性に基づいてサイズが確保されます。
|
|
41
|
+
|
|
42
|
+
## サイズ決定ルール
|
|
43
|
+
|
|
44
|
+
サイズの決定タイミングは hydrate の前後で異なります。
|
|
45
|
+
|
|
46
|
+
### hydrate 前 (`pixiv-icon:not(:defined)` の段階)
|
|
47
|
+
|
|
48
|
+
1. **`style="--charcoal-icon-size: Npx"` が指定されていれば、その値を使う**(最優先)
|
|
49
|
+
2. それ以外は `name` プレフィックスと `scale` 属性から導出されたガイドラインデフォルトを使う
|
|
50
|
+
|
|
51
|
+
`fixed-size` / `unsafe-non-guideline-scale` 属性は CSS の `attr()` での
|
|
52
|
+
数値解釈に制約があるためこの段階では参照できず、暫定値が表示されます。
|
|
53
|
+
**ガイドライン外のサイズで CLS を完全に防ぐには、必ず `style="--charcoal-icon-size: Npx"` をインラインで併記してください** (React の `<Icon fixedSize>` 経由なら自動で付与されます)。
|
|
54
|
+
|
|
55
|
+
### hydrate 後 (`:defined` 状態)
|
|
56
|
+
|
|
57
|
+
JS が読み込まれて web component が定義されると、`render()` が以下の優先順で size を計算し、
|
|
58
|
+
**結果を `--charcoal-icon-size` の inline style に上書きします**。
|
|
59
|
+
|
|
60
|
+
1. `fixed-size` 属性(最優先 / ピクセル値)
|
|
61
|
+
2. `unsafe-non-guideline-scale` 属性 × `name` のベースサイズ(**deprecated**)
|
|
62
|
+
3. `name` プレフィックスと `scale` 属性から導出されたガイドラインデフォルト
|
|
63
|
+
|
|
64
|
+
つまりユーザーが書いた `style="--charcoal-icon-size: ..."` は **hydrate 後には属性ベースの計算結果で上書きされる** 点に注意してください。永続的に上書きしたい場合は属性で指定します。
|
|
53
65
|
|
|
54
66
|
```html
|
|
67
|
+
<!-- ガイドライン通り 24px -->
|
|
68
|
+
<pixiv-icon name="24/Add"></pixiv-icon>
|
|
69
|
+
|
|
70
|
+
<!-- ガイドライン通り 48px (24 × scale=2) -->
|
|
71
|
+
<pixiv-icon name="24/Add" scale="2"></pixiv-icon>
|
|
72
|
+
|
|
73
|
+
<!-- 12px に固定 (hydrate 後も 12px のまま)。SSR の CLS を避けるため style も併記 -->
|
|
55
74
|
<pixiv-icon
|
|
56
75
|
name="24/Add"
|
|
57
|
-
|
|
58
|
-
style="--
|
|
76
|
+
fixed-size="12"
|
|
77
|
+
style="--charcoal-icon-size: 12px"
|
|
78
|
+
></pixiv-icon>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### `scale` 属性のプレフィックスごとの挙動
|
|
82
|
+
|
|
83
|
+
`name` プレフィックスごとに `scale` のサポート範囲が異なります。表に整理すると以下の通りです。
|
|
84
|
+
|
|
85
|
+
| プレフィックス | `scale=1` | `scale=2` | `scale=3` | 備考 |
|
|
86
|
+
| -------------- | --------- | --------- | --------- | --------------------------------- |
|
|
87
|
+
| `16/` | 16 | **16** | **16** | `scale` を無視(意図的な仕様) |
|
|
88
|
+
| `24/` | 24 | 48 | 72 | すべて対応 |
|
|
89
|
+
| `32/` | 32 | **32** | **32** | `scale` を無視(意図的な仕様) |
|
|
90
|
+
| `Inline/` | 16 | 32 | **16** | `scale=3` は `scale=1` と同じ扱い |
|
|
91
|
+
|
|
92
|
+
`scale` に対応していない組み合わせを書いても **CLS は発生しません**(CSS と JS 双方で同じ結果を返すように揃えてあります)が、期待通りに大きくならないことに注意してください。
|
|
93
|
+
大きく表示したい場合は次節の「ガイドライン外のサイズ指定」を使ってください。
|
|
94
|
+
|
|
95
|
+
### ガイドライン外のサイズ指定
|
|
96
|
+
|
|
97
|
+
ガイドライン外のサイズを使う場合は **`fixed-size` (vanilla HTML) / `fixedSize` (React)** を使います。
|
|
98
|
+
`fixed-size` は他のサイズ指定 (`scale` / `unsafe-non-guideline-scale`) よりも常に優先されます。
|
|
99
|
+
|
|
100
|
+
#### React (`<Icon fixedSize={N}>`)
|
|
101
|
+
|
|
102
|
+
```jsx
|
|
103
|
+
<Icon name="24/Add" fixedSize={12} /> {/* 12px 固定 */}
|
|
104
|
+
<Icon name="24/Add" fixedSize={40} /> {/* 40px 固定 */}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`<Icon>` は同じ値を `--charcoal-icon-size` インライン CSS 変数として自動付与するため、
|
|
108
|
+
**hydrate 前後とも正しいサイズで CLS は起きません**。利用者側で追加の対応は不要です。
|
|
109
|
+
|
|
110
|
+
#### vanilla HTML (`<pixiv-icon fixed-size="N">`)
|
|
111
|
+
|
|
112
|
+
CSS の `attr()` 関数は数値や `<length>` 型としての解釈をまだ十分にサポートしていないため、
|
|
113
|
+
**`fixed-size` 属性だけではブラウザは hydrate 前にサイズを決定できません**。
|
|
114
|
+
SSR 段階から CLS を防ぐには **必ずインラインスタイルで `--charcoal-icon-size` も同じ値を指定してください**。
|
|
115
|
+
|
|
116
|
+
```html
|
|
117
|
+
<!-- ✅ OK: fixed-size と --charcoal-icon-size の両方を指定 -->
|
|
118
|
+
<pixiv-icon
|
|
119
|
+
name="24/Add"
|
|
120
|
+
fixed-size="12"
|
|
121
|
+
style="--charcoal-icon-size: 12px;"
|
|
59
122
|
></pixiv-icon>
|
|
123
|
+
|
|
124
|
+
<!-- ❌ NG: fixed-size だけだと CSS-only 段階で name 由来の 24px が表示され、
|
|
125
|
+
JS upgrade 後に 12px へジャンプする (= CLS) -->
|
|
126
|
+
<pixiv-icon name="24/Add" fixed-size="12"></pixiv-icon>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`fixed-size` 属性は Web Component の upgrade 後に **必ず** 反映され、
|
|
130
|
+
`--charcoal-icon-size` インラインスタイルは upgrade 前の CSS-only 状態でサイズを確定させる役割を果たします。
|
|
131
|
+
両者の値が一致していれば、SSR から hydrate 完了までを通じて同じサイズで表示されます。
|
|
132
|
+
|
|
133
|
+
サーバーサイドのテンプレートで両方を出し分けるユーティリティを用意するか、React の `<Icon fixedSize>` を経由するのが安全です。
|
|
134
|
+
|
|
135
|
+
#### 非推奨: `unsafe-non-guideline-scale`
|
|
136
|
+
|
|
137
|
+
過去には `unsafe-non-guideline-scale` 属性でガイドライン外の倍率を指定できましたが、
|
|
138
|
+
これも `fixed-size` と同じ理由で CSS-only 段階ではサイズを決定できません。
|
|
139
|
+
**新規利用は `fixed-size` への移行を推奨します**。
|
|
140
|
+
|
|
141
|
+
既存利用箇所では、以下のように個別にインラインスタイルでサイズを確定させる必要があります。
|
|
142
|
+
|
|
143
|
+
```html
|
|
60
144
|
<pixiv-icon
|
|
61
145
|
name="24/Add"
|
|
62
146
|
unsafe-non-guideline-scale="0.5"
|
|
63
|
-
style="
|
|
147
|
+
style="--charcoal-icon-size: 12px;"
|
|
64
148
|
></pixiv-icon>
|
|
65
149
|
```
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export type IconSizing =
|
|
2
|
+
| {
|
|
3
|
+
scale?: 1 | 2 | 3 | '1' | '2' | '3'
|
|
4
|
+
/** @deprecated `fixedSize` を利用してください。 */
|
|
5
|
+
unsafeNonGuidelineScale?: never
|
|
6
|
+
fixedSize?: never
|
|
7
|
+
}
|
|
8
|
+
| {
|
|
9
|
+
scale?: never
|
|
10
|
+
/** @deprecated `fixedSize` を利用してください。 */
|
|
11
|
+
unsafeNonGuidelineScale: number
|
|
12
|
+
fixedSize?: never
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
scale?: never
|
|
16
|
+
/** @deprecated `fixedSize` を利用してください。 */
|
|
17
|
+
unsafeNonGuidelineScale?: never
|
|
18
|
+
fixedSize: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const isPositiveFinite = (value: unknown): value is number =>
|
|
22
|
+
typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
23
|
+
|
|
24
|
+
const parseIconName = (name: string): { size: string; baseSize: number } => {
|
|
25
|
+
if (!name.includes('/')) {
|
|
26
|
+
throw new TypeError(
|
|
27
|
+
`"${name}" is not a valid icon name. "name" must be named like [size]/[Name].`,
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const [size] = name.split('/')
|
|
32
|
+
|
|
33
|
+
if (size === 'Inline') {
|
|
34
|
+
return { size, baseSize: 16 }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const baseSize = parseInt(size, 10)
|
|
38
|
+
if (Number.isNaN(baseSize) || baseSize <= 0) {
|
|
39
|
+
throw new TypeError(
|
|
40
|
+
`"${name}" has invalid size prefix "${size}". Must be "Inline" or a positive number.`,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { size, baseSize }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function inlineSize(scale: number): number {
|
|
48
|
+
switch (scale) {
|
|
49
|
+
case 2:
|
|
50
|
+
return 32
|
|
51
|
+
default:
|
|
52
|
+
return 16
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function guidelineSize24(scale: number): number {
|
|
57
|
+
return 24 * scale
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// fixedSize > unsafeNonGuidelineScale > scale の優先順位で生のサイズを算出する。
|
|
61
|
+
// 戻り値の最終 validation は呼び出し元 (calcActualSize) で行う。
|
|
62
|
+
const resolveSize = ({
|
|
63
|
+
name,
|
|
64
|
+
scale,
|
|
65
|
+
unsafeNonGuidelineScale,
|
|
66
|
+
fixedSize,
|
|
67
|
+
}: { name: string } & IconSizing): number => {
|
|
68
|
+
// fixedSize (px 直接指定) が最優先
|
|
69
|
+
if (isPositiveFinite(fixedSize)) {
|
|
70
|
+
return fixedSize
|
|
71
|
+
}
|
|
72
|
+
if (fixedSize !== undefined) {
|
|
73
|
+
throw new TypeError(
|
|
74
|
+
`fixedSize must be a positive finite number, got ${fixedSize}`,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const { size, baseSize } = parseIconName(name)
|
|
79
|
+
|
|
80
|
+
// unsafeNonGuidelineScale (deprecated) が次に優先
|
|
81
|
+
if (isPositiveFinite(unsafeNonGuidelineScale)) {
|
|
82
|
+
return baseSize * unsafeNonGuidelineScale
|
|
83
|
+
}
|
|
84
|
+
if (unsafeNonGuidelineScale !== undefined) {
|
|
85
|
+
throw new TypeError(
|
|
86
|
+
`unsafeNonGuidelineScale must be a positive finite number, got ${unsafeNonGuidelineScale}`,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ガイドライン scale
|
|
91
|
+
const numericScale = parseInt(`${scale ?? '1'}`, 10)
|
|
92
|
+
switch (size) {
|
|
93
|
+
case 'Inline':
|
|
94
|
+
return inlineSize(numericScale)
|
|
95
|
+
case '24':
|
|
96
|
+
return guidelineSize24(numericScale)
|
|
97
|
+
default:
|
|
98
|
+
return baseSize
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const calcActualSize = (
|
|
103
|
+
params: { name: string } & IconSizing,
|
|
104
|
+
): number => {
|
|
105
|
+
const actualSize = resolveSize(params)
|
|
106
|
+
|
|
107
|
+
// 全 return パスの結果が正の有限数であることを Single Source of Truth として保証する
|
|
108
|
+
if (!isPositiveFinite(actualSize)) {
|
|
109
|
+
throw new TypeError(
|
|
110
|
+
`icon size must be a positive finite number, got ${actualSize}`,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return actualSize
|
|
115
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PixivIcon } from './PixivIcon'
|
|
2
2
|
import { __SERVER__ } from './ssr'
|
|
3
3
|
export { PixivIcon, type KnownIconType, type Props } from './PixivIcon'
|
|
4
|
+
export { calcActualSize, type IconSizing } from './calcActualSize'
|
|
4
5
|
export { KNOWN_ICON_FILES } from './charcoalIconFiles'
|
|
5
6
|
export { PixivIconLoadError } from './loaders/PixivIconLoadError'
|
|
6
7
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import type { SVGProps } from 'react'
|
|
3
|
+
import { Ref, forwardRef } from 'react'
|
|
4
|
+
const SvgDescription = (
|
|
5
|
+
props: SVGProps<SVGSVGElement>,
|
|
6
|
+
ref: Ref<SVGSVGElement>,
|
|
7
|
+
) => (
|
|
8
|
+
<svg
|
|
9
|
+
width={16}
|
|
10
|
+
height={16}
|
|
11
|
+
viewBox="0 0 16 16"
|
|
12
|
+
fill="none"
|
|
13
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
14
|
+
ref={ref}
|
|
15
|
+
{...props}
|
|
16
|
+
>
|
|
17
|
+
<path
|
|
18
|
+
fillRule="evenodd"
|
|
19
|
+
clipRule="evenodd"
|
|
20
|
+
d="M12.707 5.207a1 1 0 01.293.707V12a2 2 0 01-2 2H5a2 2 0 01-2-2V4a2 2 0 012-2h4.086a1 1 0 01.707.293l2.914 2.914zM6 10a.5.5 0 000 1h4a.5.5 0 000-1H6zm0-2a.5.5 0 000 1h4a.5.5 0 000-1H6zm2.5-2.5a1 1 0 001 1H12L8.5 3v2.5z"
|
|
21
|
+
fill="currentColor"
|
|
22
|
+
/>
|
|
23
|
+
</svg>
|
|
24
|
+
)
|
|
25
|
+
export const IconDescription16: ReturnType<
|
|
26
|
+
typeof React.forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>
|
|
27
|
+
> = forwardRef(SvgDescription)
|
|
28
|
+
export default IconDescription16
|
|
@@ -11,6 +11,7 @@ import { IconBookmarkOff16 } from './16/BookmarkOff'
|
|
|
11
11
|
import { IconBookmarkOn16 } from './16/BookmarkOn'
|
|
12
12
|
import { IconCheck16 } from './16/Check'
|
|
13
13
|
import { IconComment16 } from './16/Comment'
|
|
14
|
+
import { IconDescription16 } from './16/Description'
|
|
14
15
|
import { IconDot16 } from './16/Dot'
|
|
15
16
|
import { IconDown16 } from './16/Down'
|
|
16
17
|
import { IconError16 } from './16/Error'
|
|
@@ -632,6 +633,11 @@ export default {
|
|
|
632
633
|
<code><IconDelete32 /></code>
|
|
633
634
|
</div>
|
|
634
635
|
|
|
636
|
+
<div>
|
|
637
|
+
<IconDescription16 />
|
|
638
|
+
<code><IconDescription16 /></code>
|
|
639
|
+
</div>
|
|
640
|
+
|
|
635
641
|
<div>
|
|
636
642
|
<IconDescription24 />
|
|
637
643
|
<code><IconDescription24 /></code>
|
package/src/react/v1/index.tsx
CHANGED
|
@@ -8,6 +8,7 @@ export { IconBookmarkOff16 } from './16/BookmarkOff'
|
|
|
8
8
|
export { IconBookmarkOn16 } from './16/BookmarkOn'
|
|
9
9
|
export { IconCheck16 } from './16/Check'
|
|
10
10
|
export { IconComment16 } from './16/Comment'
|
|
11
|
+
export { IconDescription16 } from './16/Description'
|
|
11
12
|
export { IconDot16 } from './16/Dot'
|
|
12
13
|
export { IconDown16 } from './16/Down'
|
|
13
14
|
export { IconError16 } from './16/Error'
|