@charcoal-ui/react 6.0.0-rc.4 → 6.0.0-rc.5

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 (37) hide show
  1. package/dist/_lib/useIsomorphicLayoutEffect.d.ts +6 -0
  2. package/dist/_lib/useIsomorphicLayoutEffect.d.ts.map +1 -0
  3. package/dist/components/Carousel/index.d.ts +2 -6
  4. package/dist/components/Carousel/index.d.ts.map +1 -1
  5. package/dist/components/Carousel/useCarouselScroller.d.ts.map +1 -1
  6. package/dist/components/Modal/index.d.ts.map +1 -1
  7. package/dist/components/Modal/useTransitionPresence.d.ts +19 -0
  8. package/dist/components/Modal/useTransitionPresence.d.ts.map +1 -0
  9. package/dist/index.cjs +2 -2
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.css +100 -47
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/layered.css +100 -47
  17. package/dist/layered.css.map +1 -1
  18. package/package.json +5 -6
  19. package/src/__tests__/token-v1-compat.test.ts +0 -1
  20. package/src/_lib/useIsomorphicLayoutEffect.ts +7 -0
  21. package/src/components/Carousel/MIGRATION.md +39 -39
  22. package/src/components/Carousel/__snapshots__/index.css.snap +39 -37
  23. package/src/components/Carousel/index.css +45 -44
  24. package/src/components/Carousel/index.story.tsx +87 -68
  25. package/src/components/Carousel/index.test.tsx +68 -47
  26. package/src/components/Carousel/index.tsx +23 -14
  27. package/src/components/Carousel/useCarouselScroller.ts +8 -12
  28. package/src/components/FieldLabel/__snapshots__/index.css.snap +1 -1
  29. package/src/components/FieldLabel/index.css +1 -1
  30. package/src/components/IconButton/__snapshots__/index.css.snap +1 -1
  31. package/src/components/IconButton/index.css +1 -1
  32. package/src/components/Modal/__snapshots__/index.css.snap +32 -1
  33. package/src/components/Modal/index.css +34 -0
  34. package/src/components/Modal/index.test.tsx +200 -0
  35. package/src/components/Modal/index.tsx +48 -78
  36. package/src/components/Modal/useTransitionPresence.ts +95 -0
  37. package/src/index.ts +0 -1
@@ -0,0 +1,7 @@
1
+ import { useEffect, useLayoutEffect } from 'react'
2
+
3
+ /**
4
+ * SSR では useLayoutEffect が警告を出すため、サーバーでは useEffect にフォールバックする
5
+ */
6
+ export const useIsomorphicLayoutEffect =
7
+ typeof window !== 'undefined' ? useLayoutEffect : useEffect
@@ -5,7 +5,9 @@
5
5
 
6
6
  ## 移行の要点
7
7
 
8
- - **import 差し替え + `children` → `items` がほぼ唯一の必須変更**。
8
+ - **import 差し替えがほぼ唯一の必須変更**。`children` はそのまま渡せる(ただし自前の
9
+ `flex` ラッパーは外して **1 スライド 1 直接子要素**にし、ラッパーの `gap` は
10
+ 各スライドの `margin` に置き換える)。
9
11
  - `onScroll` / `onResize` / `onScrollStateChange` / `ref`(`resetScroll()`)/ `defaultScroll` /
10
12
  `hasGradient` は **sandbox と同じシグネチャ**で利用できる(drop-in 互換)。
11
13
  - `scrollAmountCoef` は `scrollStep`(関数も可)に名称変更。`fadeInGradient` / `buttonOffset` 系 /
@@ -17,7 +19,7 @@
17
19
  | | sandbox (`@charcoal-ui/react-sandbox`) | react (`@charcoal-ui/react`) |
18
20
  | -------------- | -------------------------------------- | ----------------------------------------------- |
19
21
  | スクロール | react-spring による JS アニメーション | ネイティブ overflow + CSS `scroll-snap` |
20
- | 子要素 | `children`(任意のノード) | `items: { id; children }[]` |
22
+ | 子要素 | `children`(任意のノード) | `children`(1 直接子要素 = 1 スライド) |
21
23
  | インジケーター | なし | CSS `::scroll-marker` / JS フォールバックの dot |
22
24
  | スタイル | styled-components | プレーン CSS(`index.css`) |
23
25
 
@@ -28,14 +30,15 @@
28
30
  + import { Carousel } from '@charcoal-ui/react'
29
31
  ```
30
32
 
31
- 公開型: `CarouselProps` / `CarouselItem` / `CarouselHandlerRef` / `ScrollAlign` / `ScrollStep` /
33
+ 公開型: `CarouselProps` / `CarouselHandlerRef` / `ScrollAlign` / `ScrollStep` /
32
34
  `ScrollStepContext` / `ScrollSnap` / `ScrollSnapType` / `ScrollSnapAlign`。
33
35
 
34
- ## 子要素: `children` → `items`
36
+ ## 子要素: `children`(そのまま渡せる)
35
37
 
36
- 最大の変更点。子ノードを直接渡す形から、`id` 付きの配列に変える。
37
- sandbox では自前で `flex` ラッパーや `gap` を組んでいたが、新版はレイアウト(`gap: 24px`、S は `0`)を
38
- コンポーネント側 CSS が持つので、**ラッパーで囲まず 1 スライド 1 要素**で渡す。
38
+ sandbox と同じく子ノードを直接渡し、スライドの寸法・間隔も sandbox 同様に利用者が持つ
39
+ (コンポーネントの責務はスクロールと indicator / arrow によるページ送りのみ)。
40
+ ただし**直接子要素 1 つを 1 スライド**として数えるので、`flex` ラッパーは外し、
41
+ `gap` の代わりに各スライドの `margin` などで間隔を注入する。
39
42
 
40
43
  ```diff
41
44
  - <Carousel hasGradient defaultScroll={{ align: 'center' }} scrollAmountCoef={0.75}>
@@ -45,38 +48,36 @@ sandbox では自前で `flex` ラッパーや `gap` を組んでいたが、新
45
48
  - ))}
46
49
  - </div>
47
50
  - </Carousel>
48
- + <Carousel
49
- + size="M"
50
- + hasGradient
51
- + defaultScroll={{ align: 'center' }}
52
- + scrollStep={0.75}
53
- + items={items.map((i) => ({ id: String(i), children: <Slide /> }))}
54
- + />
51
+ + <Carousel hasGradient defaultScroll={{ align: 'center' }} scrollStep={0.75}>
52
+ + {items.map((i) => (
53
+ + <Slide key={i} style={{ marginInlineEnd: 8 }} />
54
+ + ))}
55
+ + </Carousel>
55
56
  ```
56
57
 
57
- `id` はキー安定化(再レンダー・スクロール命令の宛先識別)に使うので、配列内で一意かつ安定した値にする。
58
+ `key` は再レンダー時のスライド識別に使うので、一意かつ安定した値にする。
58
59
 
59
60
  ## props 対応表
60
61
 
61
- | sandbox | react | 備考 |
62
- | ------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
63
- | `children` | `items: { id: string; children: ReactNode }[]` | 上記参照 |
64
- | `scrollAmountCoef`(既定 `0.75`) | `scrollStep`(既定 `0.75`) | `number`(表示幅比)に加え `(ctx) => px` の関数も渡せる |
65
- | `defaultScroll: { align, offset }` | `defaultScroll: { align, offset }` | `align` は `'left' \| 'center' \| 'right'`。ほぼ同等 |
66
- | `hasGradient` | `hasGradient`(既定 `false`) | 実装が mask → 背景色オーバーレイに変更 |
67
- | `fadeInGradient` | (廃止) | 常にオーバーレイ式フェード |
68
- | `buttonOffset` / `buttonPadding` / `bottomOffset` | (廃止) | ボタン配置は CSS グリッド(左右 72px ゾーン)に固定 |
69
- | `centerItems` | (廃止) | レイアウトは `flex` + `gap` 固定 |
70
- | `onScroll(left)` | `onScroll(left)` | ✅ そのまま対応(scroll で発火) |
71
- | `onResize(width)` | `onResize(width)` | ✅ scroller 幅の変化で発火 |
72
- | `onScrollStateChange(canScroll)` | `onScrollStateChange(canScroll)` | ✅ `canPrev \|\| canNext` の変化で発火 |
73
- | `ref`(`CarouselHandlerRef.resetScroll()`) | `ref`(`CarouselHandlerRef.resetScroll()`) | ✅ `forwardRef` で対応。`defaultScroll` の初期位置へ戻す |
74
- | — | `size: 'S' \| 'M'`(既定 `'M'`) | 新規。`S` は 1 枚全幅 + `mandatory` スナップ |
75
- | — | `navigationButtons?: boolean` | 既定は `size === 'M'` |
76
- | — | `indicator?: boolean` | 既定は `size === 'S'` |
77
- | — | `fullWidth?: boolean`(既定 `false`) | `100vw` 表示 |
78
- | — | `className?: string` | ルートに付与 |
79
- | — | `scrollSnap?: { type?; align? }` | `type`: `none`/`proximity`/`mandatory`、`align`: `center`/`start`。未指定で M=none / S=mandatory / center |
62
+ | sandbox | react | 備考 |
63
+ | ------------------------------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
64
+ | `children` | `children` | そのまま対応(1 直接子要素 = 1 スライド。ラッパーは外す。上記参照) |
65
+ | `scrollAmountCoef`(既定 `0.75`) | `scrollStep`(既定 `0.75`) | `number`(表示幅比)に加え `(ctx) => px` の関数も渡せる |
66
+ | `defaultScroll: { align, offset }` | `defaultScroll: { align, offset }` | `align` は `'left' \| 'center' \| 'right'`。ほぼ同等 |
67
+ | `hasGradient` | `hasGradient`(既定 `false`) | そのまま対応(mask による透過フェード) |
68
+ | `fadeInGradient` | (廃止) | スクロール可能な側のみ常にフェード |
69
+ | `buttonOffset` / `buttonPadding` / `bottomOffset` | (廃止) | ボタン配置は CSS グリッド(左右 72px ゾーン)に固定 |
70
+ | `centerItems` | (廃止) | スライドの寸法・間隔は children 側で注入する(sandbox 同様) |
71
+ | `onScroll(left)` | `onScroll(left)` | ✅ そのまま対応(scroll で発火) |
72
+ | `onResize(width)` | `onResize(width)` | ✅ scroller 幅の変化で発火 |
73
+ | `onScrollStateChange(canScroll)` | `onScrollStateChange(canScroll)` | ✅ `canPrev \|\| canNext` の変化で発火 |
74
+ | `ref`(`CarouselHandlerRef.resetScroll()`) | `ref`(`CarouselHandlerRef.resetScroll()`) | ✅ `forwardRef` で対応。`defaultScroll` の初期位置へ戻す |
75
+ | — | `size: 'S' \| 'M'`(既定 `'M'`) | 新規。`S` は 1 枚全幅 + `mandatory` スナップ |
76
+ | — | `navigationButtons?: boolean` | 既定は `size === 'M'` |
77
+ | — | `indicator?: boolean` | 既定は `size === 'S'` |
78
+ | — | `fullWidth?: boolean`(既定 `false`) | `100vw` 表示 |
79
+ | — | `className?: string` | ルートに付与 |
80
+ | — | `scrollSnap?: { type?; align? }` | `type`: `none`/`proximity`/`mandatory`、`align`: `center`/`start`。未指定で M=none / S=mandatory / center |
80
81
 
81
82
  ## 挙動の変更(移行時に確認すること)
82
83
 
@@ -92,8 +93,6 @@ sandbox では自前で `flex` ラッパーや `gap` を組んでいたが、新
92
93
  非対応環境では JS フォールバック)。
93
94
  - **キーボード操作**: スクローラーが `tabIndex={0}` でフォーカス可能になり、`←` / `→` で 1 ステップスクロール。
94
95
  フォーカスリングは charcoal 標準(`box-shadow: 0 0 0 4px rgba(0, 150, 250, 0.32)`)。
95
- - **グラデーション**: `mask` による透過から、背景色(`#fff`)オーバーレイ方式に変更。
96
- ダークモードは現状未対応(背景色固定)。
97
96
 
98
97
  ## スクロール量を細かく制御したい場合
99
98
 
@@ -101,17 +100,18 @@ sandbox では自前で `flex` ラッパーや `gap` を組んでいたが、新
101
100
 
102
101
  ```tsx
103
102
  // 表示幅の比率(sandbox の scrollAmountCoef 相当)
104
- <Carousel items={items} scrollStep={0.5} />
103
+ <Carousel scrollStep={0.5}>{slides}</Carousel>
105
104
 
106
105
  // 進む px を直接返す(端は残り全部、など)
107
106
  <Carousel
108
- items={items}
109
107
  scrollStep={({ clientWidth, scrollWidth, scrollLeft, direction }) =>
110
108
  direction === 'next'
111
109
  ? Math.min(clientWidth * 0.8, scrollWidth - clientWidth - scrollLeft)
112
110
  : Math.min(clientWidth * 0.8, scrollLeft)
113
111
  }
114
- />
112
+ >
113
+ {slides}
114
+ </Carousel>
115
115
  ```
116
116
 
117
117
  戻り値は「進む量の絶対値(px)」。符号(prev / next)はコンポーネント側で付与する。
@@ -12,10 +12,19 @@
12
12
  position: relative;
13
13
  }
14
14
 
15
+ /* フォーカスリングは scroller ではなく viewport に描く。scroller に置くと
16
+ hasGradient の mask(mask-clip: border-box)で外側の box-shadow ごと消えるため。 */
17
+
18
+ .charcoal-carousel__viewport[data-focus-visible] {
19
+ box-shadow: 0 0 0 4px rgba(0, 150, 250, 0.32);
20
+ }
21
+
22
+ /* スライド間隔は所有しない(sandbox 同様、利用者が children 側で注入する)。
23
+ このコンポーネントの責務はスクロールと indicator / arrow によるページ送りのみ。 */
24
+
15
25
  .charcoal-carousel__scroller {
16
26
  position: relative;
17
27
  display: flex;
18
- gap: 24px;
19
28
  overflow-x: auto;
20
29
  overflow-y: hidden;
21
30
  overscroll-behavior-x: contain;
@@ -28,14 +37,6 @@
28
37
  display: none;
29
38
  }
30
39
 
31
- .charcoal-carousel__scroller[data-focus-visible] {
32
- box-shadow: 0 0 0 4px rgba(0, 150, 250, 0.32);
33
- }
34
-
35
- .charcoal-carousel[data-size='S'] .charcoal-carousel__scroller {
36
- gap: 0;
37
- }
38
-
39
40
  /* スクロールスナップ type は prop(scrollSnap.type)で scroller に出し分ける。
40
41
  align は基底 .charcoal-carousel__item の後(後述)に置く(no-descending-specificity 回避)。
41
42
  未指定時の既定(M=none / S=mandatory / align=center)は JS 側で data 属性に解決する。 */
@@ -52,38 +53,39 @@
52
53
  scroll-snap-type: x mandatory;
53
54
  }
54
55
 
55
- /* グラデーション: はみ出してスクロール可能な側のみ、端のアイテムを背景色(白)へ
56
- フェードさせる。mask による透過ではなく、72px の「背景色→透明」オーバーレイを
57
- ビューポートに重ねて白で覆う。スクロール可能な側だけ data-can-prev/next で出し分ける。 */
56
+ /* グラデーション: mask で端のアイテム自体を透明へフェードさせる(下の背景色を
57
+ 問わない)。mask の色はアルファとしてだけ効く(#000=表示 / transparent=透過)。
58
+ 左フェード・右フェードの 2 レイヤーを intersect で合成し、はみ出して
59
+ スクロール可能な側のみ data-can-prev/next で 72px のフェード帯を枠内に置く。
60
+ 無効な側はフェード帯ごと枠外へスライドして隠す(mask-image と違い
61
+ mask-position は補間可能なので、切替が 0.2s のフェードになる)。 */
58
62
 
59
- .charcoal-carousel[data-has-gradient='true'] .charcoal-carousel__viewport::before, .charcoal-carousel[data-has-gradient='true'] .charcoal-carousel__viewport::after {
60
- content: '';
61
- position: absolute;
62
- top: 0;
63
- bottom: 0;
64
- width: 72px;
65
- pointer-events: none;
66
- opacity: 0;
67
- transition: 0.2s opacity;
68
- z-index: 1;
63
+ .charcoal-carousel[data-has-gradient='true'] .charcoal-carousel__scroller {
64
+ mask-image: linear-gradient(to right, transparent, #000 72px),
65
+ linear-gradient(to left, transparent, #000 72px);
66
+ /* 枠外へ 72px スライドしても反対側の端が欠けないよう、レイヤー幅に 72px 足す */
67
+ mask-size: calc(100% + 72px) 100%;
68
+ mask-repeat: no-repeat;
69
+ mask-composite: intersect;
70
+ mask-position: -72px 0,
71
+ 0 0;
72
+ /* 0.2s ease-in は react-sandbox の LeftGradient と同じ値 */
73
+ transition: 0.2s ease-in mask-position;
69
74
  }
70
75
 
71
- .charcoal-carousel[data-has-gradient='true'] .charcoal-carousel__viewport::before {
72
- left: 0;
73
- background: linear-gradient(to right, #fff, transparent);
76
+ .charcoal-carousel[data-has-gradient='true'][data-can-prev='true'] .charcoal-carousel__scroller {
77
+ mask-position: 0 0,
78
+ 0 0;
74
79
  }
75
80
 
76
- .charcoal-carousel[data-has-gradient='true'] .charcoal-carousel__viewport::after {
77
- right: 0;
78
- background: linear-gradient(to left, #fff, transparent);
81
+ .charcoal-carousel[data-has-gradient='true'][data-can-next='true'] .charcoal-carousel__scroller {
82
+ mask-position: -72px 0,
83
+ -72px 0;
79
84
  }
80
85
 
81
- .charcoal-carousel[data-has-gradient='true'][data-can-prev='true'] .charcoal-carousel__viewport::before {
82
- opacity: 1;
83
- }
84
-
85
- .charcoal-carousel[data-has-gradient='true'][data-can-next='true'] .charcoal-carousel__viewport::after {
86
- opacity: 1;
86
+ .charcoal-carousel[data-has-gradient='true'][data-can-prev='true'][data-can-next='true'] .charcoal-carousel__scroller {
87
+ mask-position: 0 0,
88
+ -72px 0;
87
89
  }
88
90
 
89
91
  .charcoal-carousel__item {
@@ -108,7 +110,7 @@
108
110
 
109
111
  /* ── Navigation Buttons (charcoal IconButton, variant=Overlay size=S) ── */
110
112
 
111
- /* マスクと同じ 72px 1fr 72px グリッド。各ボタンは 72px ゾーンの中央に置く。 */
113
+ /* マスクの透過域と同じ 72px 1fr 72px グリッド。各ボタンは 72px ゾーンの中央に置く。 */
112
114
 
113
115
  .charcoal-carousel__navigation {
114
116
  position: absolute;
@@ -117,8 +119,8 @@
117
119
  display: grid;
118
120
  grid-template-columns: 72px 1fr 72px;
119
121
  align-items: center;
120
- /* 白フェード(z-index: 1)より上にボタンを表示する */
121
- z-index: 2;
122
+ /* アイテム内のコンテンツより上にボタンを表示する */
123
+ z-index: 1;
122
124
  /* sandbox 同様、通常は隠してカルーセル hover 時にフェードイン表示する。 */
123
125
  opacity: 0;
124
126
  transition: 0.4s opacity;
@@ -10,12 +10,19 @@
10
10
 
11
11
  .charcoal-carousel__viewport {
12
12
  position: relative;
13
+
14
+ /* フォーカスリングは scroller ではなく viewport に描く。scroller に置くと
15
+ hasGradient の mask(mask-clip: border-box)で外側の box-shadow ごと消えるため。 */
16
+ &[data-focus-visible] {
17
+ box-shadow: 0 0 0 4px rgba(0, 150, 250, 0.32);
18
+ }
13
19
  }
14
20
 
21
+ /* スライド間隔は所有しない(sandbox 同様、利用者が children 側で注入する)。
22
+ このコンポーネントの責務はスクロールと indicator / arrow によるページ送りのみ。 */
15
23
  .charcoal-carousel__scroller {
16
24
  position: relative;
17
25
  display: flex;
18
- gap: 24px;
19
26
  overflow-x: auto;
20
27
  overflow-y: hidden;
21
28
  overscroll-behavior-x: contain;
@@ -26,14 +33,6 @@
26
33
  &::-webkit-scrollbar {
27
34
  display: none;
28
35
  }
29
-
30
- &[data-focus-visible] {
31
- box-shadow: 0 0 0 4px rgba(0, 150, 250, 0.32);
32
- }
33
- }
34
-
35
- .charcoal-carousel[data-size='S'] .charcoal-carousel__scroller {
36
- gap: 0;
37
36
  }
38
37
 
39
38
  /* スクロールスナップ type は prop(scrollSnap.type)で scroller に出し分ける。
@@ -53,44 +52,46 @@
53
52
  scroll-snap-type: x mandatory;
54
53
  }
55
54
 
56
- /* グラデーション: はみ出してスクロール可能な側のみ、端のアイテムを背景色(白)へ
57
- フェードさせる。mask による透過ではなく、72px の「背景色→透明」オーバーレイを
58
- ビューポートに重ねて白で覆う。スクロール可能な側だけ data-can-prev/next で出し分ける。 */
59
- .charcoal-carousel[data-has-gradient='true']
60
- .charcoal-carousel__viewport::before,
61
- .charcoal-carousel[data-has-gradient='true']
62
- .charcoal-carousel__viewport::after {
63
- content: '';
64
- position: absolute;
65
- top: 0;
66
- bottom: 0;
67
- width: 72px;
68
- pointer-events: none;
69
- opacity: 0;
70
- transition: 0.2s opacity;
71
- z-index: 1;
72
- }
73
-
74
- .charcoal-carousel[data-has-gradient='true']
75
- .charcoal-carousel__viewport::before {
76
- left: 0;
77
- background: linear-gradient(to right, #fff, transparent);
78
- }
79
-
80
- .charcoal-carousel[data-has-gradient='true']
81
- .charcoal-carousel__viewport::after {
82
- right: 0;
83
- background: linear-gradient(to left, #fff, transparent);
55
+ /* グラデーション: mask で端のアイテム自体を透明へフェードさせる(下の背景色を
56
+ 問わない)。mask の色はアルファとしてだけ効く(#000=表示 / transparent=透過)。
57
+ 左フェード・右フェードの 2 レイヤーを intersect で合成し、はみ出して
58
+ スクロール可能な側のみ data-can-prev/next で 72px のフェード帯を枠内に置く。
59
+ 無効な側はフェード帯ごと枠外へスライドして隠す(mask-image と違い
60
+ mask-position は補間可能なので、切替が 0.2s のフェードになる)。 */
61
+ .charcoal-carousel[data-has-gradient='true'] .charcoal-carousel__scroller {
62
+ mask-image:
63
+ linear-gradient(to right, transparent, #000 72px),
64
+ linear-gradient(to left, transparent, #000 72px);
65
+ /* 枠外へ 72px スライドしても反対側の端が欠けないよう、レイヤー幅に 72px 足す */
66
+ mask-size: calc(100% + 72px) 100%;
67
+ mask-repeat: no-repeat;
68
+ mask-composite: intersect;
69
+ mask-position:
70
+ -72px 0,
71
+ 0 0;
72
+ /* 0.2s ease-in は react-sandbox の LeftGradient と同じ値 */
73
+ transition: 0.2s ease-in mask-position;
84
74
  }
85
75
 
86
76
  .charcoal-carousel[data-has-gradient='true'][data-can-prev='true']
87
- .charcoal-carousel__viewport::before {
88
- opacity: 1;
77
+ .charcoal-carousel__scroller {
78
+ mask-position:
79
+ 0 0,
80
+ 0 0;
89
81
  }
90
82
 
91
83
  .charcoal-carousel[data-has-gradient='true'][data-can-next='true']
92
- .charcoal-carousel__viewport::after {
93
- opacity: 1;
84
+ .charcoal-carousel__scroller {
85
+ mask-position:
86
+ -72px 0,
87
+ -72px 0;
88
+ }
89
+
90
+ .charcoal-carousel[data-has-gradient='true'][data-can-prev='true'][data-can-next='true']
91
+ .charcoal-carousel__scroller {
92
+ mask-position:
93
+ 0 0,
94
+ -72px 0;
94
95
  }
95
96
 
96
97
  .charcoal-carousel__item {
@@ -114,7 +115,7 @@
114
115
 
115
116
  /* ── Navigation Buttons (charcoal IconButton, variant=Overlay size=S) ── */
116
117
 
117
- /* マスクと同じ 72px 1fr 72px グリッド。各ボタンは 72px ゾーンの中央に置く。 */
118
+ /* マスクの透過域と同じ 72px 1fr 72px グリッド。各ボタンは 72px ゾーンの中央に置く。 */
118
119
  .charcoal-carousel__navigation {
119
120
  position: absolute;
120
121
  inset: 0;
@@ -122,8 +123,8 @@
122
123
  display: grid;
123
124
  grid-template-columns: 72px 1fr 72px;
124
125
  align-items: center;
125
- /* 白フェード(z-index: 1)より上にボタンを表示する */
126
- z-index: 2;
126
+ /* アイテム内のコンテンツより上にボタンを表示する */
127
+ z-index: 1;
127
128
  /* sandbox 同様、通常は隠してカルーセル hover 時にフェードイン表示する。 */
128
129
  opacity: 0;
129
130
  transition: 0.4s opacity;
@@ -1,45 +1,52 @@
1
1
  import { Meta, StoryObj } from '@storybook/react-vite'
2
- import Carousel, { type CarouselItem } from '.'
3
-
4
- const sampleImage = (
5
- <img
6
- src="/carousel-sample.png"
7
- alt="サンプル画像"
8
- style={{
9
- width: '100%',
10
- height: '100%',
11
- objectFit: 'cover',
12
- display: 'block',
13
- }}
14
- />
15
- )
16
-
17
- const items: CarouselItem[] = Array.from({ length: 6 }, (_, i) => ({
18
- id: `item-${i + 1}`,
19
- children: sampleImage,
20
- }))
21
-
22
- // 横幅が確定したスロット(defaultScroll の初期位置計算と 0.75 ページ送りを確認するため)
23
- const numberedItems: CarouselItem[] = Array.from({ length: 20 }, (_, i) => ({
24
- id: `num-${i + 1}`,
25
- children: (
26
- <div
2
+ import type { CSSProperties } from 'react'
3
+ import Carousel from '.'
4
+
5
+ const makeSampleImages = (style: CSSProperties) =>
6
+ Array.from({ length: 6 }, (_, i) => (
7
+ <img
8
+ key={`item-${i + 1}`}
9
+ src="/carousel-sample.png"
10
+ alt="サンプル画像"
27
11
  style={{
28
- width: 200,
29
- height: 120,
30
- display: 'flex',
31
- alignItems: 'center',
32
- justifyContent: 'center',
33
- background: i % 2 === 0 ? '#cfe3ff' : '#ffe3cf',
34
- borderRadius: 8,
35
- font: 'bold 32px sans-serif',
36
- color: '#333',
12
+ objectFit: 'cover',
13
+ display: 'block',
14
+ ...style,
37
15
  }}
38
- >
39
- {i + 1}
40
- </div>
41
- ),
42
- }))
16
+ />
17
+ ))
18
+
19
+ // M 用: スライド間隔はコンポーネントが所有しないので、利用者側の margin で注入する。
20
+ // width: 100% と margin の併用はラッパーとの循環サイズ解決で間隔が潰れるため、明示サイズにする。
21
+ const spacedImages = makeSampleImages({
22
+ width: 280,
23
+ height: 210,
24
+ marginInlineEnd: 24,
25
+ })
26
+ // S 用: 1 枚全幅ページングのため間隔なし
27
+ const fullWidthImages = makeSampleImages({ width: '100%', height: '100%' })
28
+
29
+ // 横幅が確定したスロット(defaultScroll の初期位置計算と 0.75 ページ送りを確認するため)。
30
+ // スライド間隔はコンポーネントが所有しないので、利用者側の margin で注入する。
31
+ const numberedSlides = Array.from({ length: 20 }, (_, i) => (
32
+ <div
33
+ key={`num-${i + 1}`}
34
+ style={{
35
+ width: 200,
36
+ height: 120,
37
+ marginInlineEnd: 24,
38
+ display: 'flex',
39
+ alignItems: 'center',
40
+ justifyContent: 'center',
41
+ background: i % 2 === 0 ? '#cfe3ff' : '#ffe3cf',
42
+ borderRadius: 8,
43
+ font: 'bold 32px sans-serif',
44
+ color: '#333',
45
+ }}
46
+ >
47
+ {i + 1}
48
+ </div>
49
+ ))
43
50
 
44
51
  export default {
45
52
  title: 'react/Carousel',
@@ -48,13 +55,14 @@ export default {
48
55
  layout: 'padded',
49
56
  },
50
57
  args: {
51
- items,
58
+ children: spacedImages,
52
59
  size: 'M',
53
60
  hasGradient: false,
54
61
  fullWidth: false,
55
62
  scrollStep: 0.75,
56
63
  },
57
64
  argTypes: {
65
+ children: { control: false },
58
66
  size: {
59
67
  control: { type: 'radio' },
60
68
  options: ['S', 'M'],
@@ -80,7 +88,7 @@ export const SizeM: StoryObj<typeof Carousel> = {
80
88
  }
81
89
 
82
90
  export const SizeS: StoryObj<typeof Carousel> = {
83
- args: { size: 'S' },
91
+ args: { size: 'S', children: fullWidthImages },
84
92
  }
85
93
 
86
94
  export const WithGradient: StoryObj<typeof Carousel> = {
@@ -92,7 +100,7 @@ export const FullWidth: StoryObj<typeof Carousel> = {
92
100
  }
93
101
 
94
102
  export const NavigationButtonsOnSizeS: StoryObj<typeof Carousel> = {
95
- args: { size: 'S', navigationButtons: true },
103
+ args: { size: 'S', navigationButtons: true, children: fullWidthImages },
96
104
  }
97
105
 
98
106
  export const IndicatorOnSizeM: StoryObj<typeof Carousel> = {
@@ -101,42 +109,53 @@ export const IndicatorOnSizeM: StoryObj<typeof Carousel> = {
101
109
 
102
110
  // 横幅が確定したスロット(番号付き)
103
111
  export const DefaultScrollCenter: StoryObj<typeof Carousel> = {
104
- args: { size: 'M', items: numberedItems, defaultScroll: { align: 'center' } },
112
+ args: {
113
+ size: 'M',
114
+ children: numberedSlides,
115
+ defaultScroll: { align: 'center' },
116
+ },
105
117
  }
106
118
 
107
119
  export const DefaultScrollRight: StoryObj<typeof Carousel> = {
108
- args: { size: 'M', items: numberedItems, defaultScroll: { align: 'right' } },
120
+ args: {
121
+ size: 'M',
122
+ children: numberedSlides,
123
+ defaultScroll: { align: 'right' },
124
+ },
109
125
  }
110
126
 
111
127
  // 遅延読み込み画像(マウント時に幅が未確定)でも初期位置が効くことの確認
112
128
  export const DefaultScrollCenterAsyncImages: StoryObj<typeof Carousel> = {
113
- args: { size: 'M', items, defaultScroll: { align: 'center' } },
129
+ args: {
130
+ size: 'M',
131
+ children: spacedImages,
132
+ defaultScroll: { align: 'center' },
133
+ },
114
134
  }
115
135
 
116
136
  // 白フェードが見えることの確認用(暗色コンテンツなら白フェードが明確に出る)
117
- const darkTiles: CarouselItem[] = Array.from({ length: 10 }, (_, i) => ({
118
- id: `dark-${i + 1}`,
119
- children: (
120
- <div
121
- style={{
122
- width: 220,
123
- height: 140,
124
- display: 'flex',
125
- alignItems: 'center',
126
- justifyContent: 'center',
127
- background: '#2a3b8f',
128
- color: '#fff',
129
- borderRadius: 4,
130
- font: 'bold 28px sans-serif',
131
- }}
132
- >
133
- {i + 1}
134
- </div>
135
- ),
136
- }))
137
+ const darkTiles = Array.from({ length: 10 }, (_, i) => (
138
+ <div
139
+ key={`dark-${i + 1}`}
140
+ style={{
141
+ width: 220,
142
+ height: 140,
143
+ marginInlineEnd: 24,
144
+ display: 'flex',
145
+ alignItems: 'center',
146
+ justifyContent: 'center',
147
+ background: '#2a3b8f',
148
+ color: '#fff',
149
+ borderRadius: 4,
150
+ font: 'bold 28px sans-serif',
151
+ }}
152
+ >
153
+ {i + 1}
154
+ </div>
155
+ ))
137
156
 
138
157
  export const GradientOnDarkContent: StoryObj<typeof Carousel> = {
139
- args: { size: 'M', hasGradient: true, items: darkTiles },
158
+ args: { size: 'M', hasGradient: true, children: darkTiles },
140
159
  }
141
160
 
142
161
  // scrollStep に関数を渡してフル制御する例(送り量を clientWidth - 48px に固定)。
@@ -144,7 +163,7 @@ export const GradientOnDarkContent: StoryObj<typeof Carousel> = {
144
163
  export const ScrollStepFunction: StoryObj<typeof Carousel> = {
145
164
  args: {
146
165
  size: 'M',
147
- items: numberedItems,
166
+ children: numberedSlides,
148
167
  scrollStep: ({ clientWidth }) => clientWidth - 48,
149
168
  },
150
169
  }
@@ -153,7 +172,7 @@ export const ScrollStepFunction: StoryObj<typeof Carousel> = {
153
172
  export const ScrollSnapPerItem: StoryObj<typeof Carousel> = {
154
173
  args: {
155
174
  size: 'M',
156
- items: numberedItems,
175
+ children: numberedSlides,
157
176
  scrollSnap: { type: 'mandatory', align: 'start' },
158
177
  },
159
178
  }