@charcoal-ui/react 6.0.0-rc.0 → 6.0.0-rc.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.
Files changed (47) hide show
  1. package/dist/components/Carousel/CarouselItem.d.ts +10 -0
  2. package/dist/components/Carousel/CarouselItem.d.ts.map +1 -0
  3. package/dist/components/Carousel/carouselStore.d.ts +25 -0
  4. package/dist/components/Carousel/carouselStore.d.ts.map +1 -0
  5. package/dist/components/Carousel/index.d.ts +61 -0
  6. package/dist/components/Carousel/index.d.ts.map +1 -0
  7. package/dist/components/Carousel/intersectionObserver.d.ts +7 -0
  8. package/dist/components/Carousel/intersectionObserver.d.ts.map +1 -0
  9. package/dist/components/Carousel/resizeObserver.d.ts +6 -0
  10. package/dist/components/Carousel/resizeObserver.d.ts.map +1 -0
  11. package/dist/components/Carousel/store.d.ts +7 -0
  12. package/dist/components/Carousel/store.d.ts.map +1 -0
  13. package/dist/components/Carousel/useCarouselScroller.d.ts +18 -0
  14. package/dist/components/Carousel/useCarouselScroller.d.ts.map +1 -0
  15. package/dist/components/Modal/index.d.ts +1 -1
  16. package/dist/components/TagItem/index.d.ts +5 -4
  17. package/dist/components/TagItem/index.d.ts.map +1 -1
  18. package/dist/index.cjs +2 -2
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.css +275 -0
  21. package/dist/index.d.ts +3 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/layered.css +275 -0
  26. package/dist/layered.css.map +1 -1
  27. package/package.json +5 -5
  28. package/src/components/Carousel/CarouselItem.tsx +56 -0
  29. package/src/components/Carousel/MIGRATION.md +117 -0
  30. package/src/components/Carousel/Migration.mdx +8 -0
  31. package/src/components/Carousel/__snapshots__/index.css.snap +274 -0
  32. package/src/components/Carousel/carouselStore.test.ts +38 -0
  33. package/src/components/Carousel/carouselStore.ts +48 -0
  34. package/src/components/Carousel/index.css +274 -0
  35. package/src/components/Carousel/index.story.tsx +169 -0
  36. package/src/components/Carousel/index.test.tsx +666 -0
  37. package/src/components/Carousel/index.tsx +278 -0
  38. package/src/components/Carousel/intersectionObserver.test.ts +68 -0
  39. package/src/components/Carousel/intersectionObserver.ts +58 -0
  40. package/src/components/Carousel/resizeObserver.test.ts +60 -0
  41. package/src/components/Carousel/resizeObserver.ts +35 -0
  42. package/src/components/Carousel/store.test.ts +25 -0
  43. package/src/components/Carousel/store.ts +27 -0
  44. package/src/components/Carousel/useCarouselScroller.ts +158 -0
  45. package/src/components/TagItem/__snapshots__/index.story.storyshot +17 -17
  46. package/src/components/TagItem/index.tsx +25 -7
  47. package/src/index.ts +14 -2
@@ -0,0 +1,274 @@
1
+ .charcoal-carousel {
2
+ position: relative;
3
+ display: block;
4
+
5
+ &[data-full-width='true'] {
6
+ width: 100vw;
7
+ margin-inline: calc(50% - 50vw);
8
+ }
9
+ }
10
+
11
+ .charcoal-carousel__viewport {
12
+ position: relative;
13
+ }
14
+
15
+ .charcoal-carousel__scroller {
16
+ position: relative;
17
+ display: flex;
18
+ gap: 24px;
19
+ overflow-x: auto;
20
+ overflow-y: hidden;
21
+ overscroll-behavior-x: contain;
22
+ scroll-behavior: smooth;
23
+ scrollbar-width: none;
24
+ outline: none;
25
+
26
+ &::-webkit-scrollbar {
27
+ display: none;
28
+ }
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
+ }
38
+
39
+ /* スクロールスナップ type は prop(scrollSnap.type)で scroller に出し分ける。
40
+ align は基底 .charcoal-carousel__item の後(後述)に置く(no-descending-specificity 回避)。
41
+ 未指定時の既定(M=none / S=mandatory / align=center)は JS 側で data 属性に解決する。 */
42
+ .charcoal-carousel[data-scroll-snap-type='none'] .charcoal-carousel__scroller {
43
+ scroll-snap-type: none;
44
+ }
45
+
46
+ .charcoal-carousel[data-scroll-snap-type='proximity']
47
+ .charcoal-carousel__scroller {
48
+ scroll-snap-type: x proximity;
49
+ }
50
+
51
+ .charcoal-carousel[data-scroll-snap-type='mandatory']
52
+ .charcoal-carousel__scroller {
53
+ scroll-snap-type: x mandatory;
54
+ }
55
+
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);
84
+ }
85
+
86
+ .charcoal-carousel[data-has-gradient='true'][data-can-prev='true']
87
+ .charcoal-carousel__viewport::before {
88
+ opacity: 1;
89
+ }
90
+
91
+ .charcoal-carousel[data-has-gradient='true'][data-can-next='true']
92
+ .charcoal-carousel__viewport::after {
93
+ opacity: 1;
94
+ }
95
+
96
+ .charcoal-carousel__item {
97
+ flex: 0 0 auto;
98
+ min-width: 0;
99
+ }
100
+
101
+ .charcoal-carousel[data-size='S'] .charcoal-carousel__item {
102
+ flex: 0 0 100%;
103
+ }
104
+
105
+ /* scroll-snap-align は prop(scrollSnap.align)で出し分ける。基底 .charcoal-carousel__item
106
+ より後に置くことで no-descending-specificity を回避する。 */
107
+ .charcoal-carousel[data-scroll-snap-align='center'] .charcoal-carousel__item {
108
+ scroll-snap-align: center;
109
+ }
110
+
111
+ .charcoal-carousel[data-scroll-snap-align='start'] .charcoal-carousel__item {
112
+ scroll-snap-align: start;
113
+ }
114
+
115
+ /* ── Navigation Buttons (charcoal IconButton, variant=Overlay size=S) ── */
116
+
117
+ /* マスクと同じ 72px 1fr 72px グリッド。各ボタンは 72px ゾーンの中央に置く。 */
118
+ .charcoal-carousel__navigation {
119
+ position: absolute;
120
+ inset: 0;
121
+ pointer-events: none;
122
+ display: grid;
123
+ grid-template-columns: 72px 1fr 72px;
124
+ align-items: center;
125
+ /* 白フェード(z-index: 1)より上にボタンを表示する */
126
+ z-index: 2;
127
+ /* sandbox 同様、通常は隠してカルーセル hover 時にフェードイン表示する。 */
128
+ opacity: 0;
129
+ transition: 0.4s opacity;
130
+
131
+ &[data-visible='false'] {
132
+ display: none;
133
+ }
134
+ }
135
+
136
+ /* hover で表示。キーボード操作でも到達できるよう focus-within でも表示する。 */
137
+ .charcoal-carousel:hover .charcoal-carousel__navigation,
138
+ .charcoal-carousel:focus-within .charcoal-carousel__navigation {
139
+ opacity: 1;
140
+ }
141
+
142
+ .charcoal-carousel__navigation__item {
143
+ pointer-events: auto;
144
+
145
+ &[data-direction='prev'] {
146
+ grid-column: 1;
147
+ justify-self: center;
148
+ }
149
+
150
+ &[data-direction='next'] {
151
+ grid-column: 3;
152
+ justify-self: center;
153
+ }
154
+ }
155
+
156
+ /* スクロール端では非表示。IconButton の disabled スタイル(opacity:0.32)より
157
+ 高い詳細度で打ち消すため、ルートを前置する。 */
158
+ .charcoal-carousel .charcoal-carousel__navigation__item[data-hidden='true'] {
159
+ opacity: 0;
160
+ pointer-events: none;
161
+ }
162
+
163
+ /* タッチデバイスではナビゲーションボタンを出さず、スワイプ操作に委ねる。 */
164
+ @media (hover: none) and (pointer: coarse) {
165
+ .charcoal-carousel__navigation {
166
+ display: none;
167
+ }
168
+ }
169
+
170
+ /* ── Indicator: JS Fallback ── */
171
+
172
+ .charcoal-carousel__indicator {
173
+ display: flex;
174
+ justify-content: center;
175
+ align-items: center;
176
+ height: 40px;
177
+ gap: 8px;
178
+
179
+ &[data-visible='false'] {
180
+ display: none;
181
+ }
182
+ }
183
+
184
+ .charcoal-carousel__indicator__item {
185
+ appearance: none;
186
+ box-sizing: border-box;
187
+ width: 8px;
188
+ height: 8px;
189
+ padding: 0;
190
+ border: 0;
191
+ border-radius: 50%;
192
+ background-color: var(--charcoal-color-text-tertiary-default);
193
+ cursor: pointer;
194
+ transition: 0.2s background-color;
195
+
196
+ &:hover {
197
+ background-color: var(--charcoal-color-text-secondary-default);
198
+ }
199
+
200
+ &:focus-visible {
201
+ outline: 2px solid rgba(0, 150, 250, 0.56);
202
+ outline-offset: 2px;
203
+ }
204
+
205
+ &[data-active='true'] {
206
+ background-color: var(--charcoal-color-text-default);
207
+ }
208
+ }
209
+
210
+ /* ── CSS Scroll Markers (progressive enhancement) ── */
211
+
212
+ /* CSS Scroll Markers / Anchor Positioning は新しい仕様で、stylelint がプロパティ・
213
+ 擬似要素・擬似クラスを未知と判定するため、この @supports ブロックでは該当ルールを無効化する。 */
214
+ /* stylelint-disable property-no-unknown, selector-pseudo-element-no-unknown, selector-pseudo-class-no-unknown */
215
+ @supports (scroll-marker-group: after) {
216
+ .charcoal-carousel[data-indicator='true'] {
217
+ padding-bottom: 40px;
218
+ }
219
+
220
+ .charcoal-carousel__scroller {
221
+ anchor-name: --charcoal-carousel;
222
+ scroll-marker-group: after;
223
+
224
+ &::scroll-button(*) {
225
+ content: none;
226
+ }
227
+
228
+ &::scroll-marker-group {
229
+ display: flex;
230
+ justify-content: center;
231
+ align-items: center;
232
+ height: 40px;
233
+ gap: 8px;
234
+
235
+ position: absolute;
236
+ position-anchor: --charcoal-carousel;
237
+ top: anchor(bottom);
238
+ justify-self: anchor-center;
239
+ }
240
+ }
241
+
242
+ .charcoal-carousel[data-indicator='false'] .charcoal-carousel__scroller {
243
+ scroll-marker-group: none;
244
+ }
245
+
246
+ .charcoal-carousel__item::scroll-marker {
247
+ content: '';
248
+ width: 8px;
249
+ height: 8px;
250
+ border: 0;
251
+ border-radius: 50%;
252
+ background-color: var(--charcoal-color-text-tertiary-default);
253
+ cursor: pointer;
254
+ transition: 0.2s background-color;
255
+
256
+ &:hover {
257
+ background-color: var(--charcoal-color-text-secondary-default);
258
+ }
259
+
260
+ &:focus {
261
+ outline: 2px solid rgba(0, 150, 250, 0.56);
262
+ outline-offset: 2px;
263
+ }
264
+
265
+ &:target-current {
266
+ background-color: var(--charcoal-color-text-default);
267
+ }
268
+ }
269
+
270
+ .charcoal-carousel__indicator {
271
+ display: none;
272
+ }
273
+ }
274
+ /* stylelint-enable property-no-unknown, selector-pseudo-element-no-unknown, selector-pseudo-class-no-unknown */
@@ -0,0 +1,169 @@
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
27
+ 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',
37
+ }}
38
+ >
39
+ {i + 1}
40
+ </div>
41
+ ),
42
+ }))
43
+
44
+ export default {
45
+ title: 'react/Carousel',
46
+ component: Carousel,
47
+ parameters: {
48
+ layout: 'padded',
49
+ },
50
+ args: {
51
+ items,
52
+ size: 'M',
53
+ hasGradient: false,
54
+ fullWidth: false,
55
+ scrollStep: 0.75,
56
+ },
57
+ argTypes: {
58
+ size: {
59
+ control: { type: 'radio' },
60
+ options: ['S', 'M'],
61
+ },
62
+ hasGradient: { control: 'boolean' },
63
+ fullWidth: { control: 'boolean' },
64
+ navigationButtons: { control: 'boolean' },
65
+ indicator: { control: 'boolean' },
66
+ // react-sandbox 互換コールバックを Actions パネルで確認できるようにする。
67
+ onScroll: { action: 'onScroll' },
68
+ onResize: { action: 'onResize' },
69
+ onScrollStateChange: { action: 'onScrollStateChange' },
70
+ scrollStep: {
71
+ // number は clientWidth に対する比率。関数 (ctx) => px も渡せるが、
72
+ // Controls では数値(比率)のみ操作可能。関数形式は ScrollStepFunction story を参照。
73
+ control: { type: 'range', min: 0.1, max: 1.5, step: 0.05 },
74
+ },
75
+ },
76
+ } satisfies Meta<typeof Carousel>
77
+
78
+ export const SizeM: StoryObj<typeof Carousel> = {
79
+ args: { size: 'M' },
80
+ }
81
+
82
+ export const SizeS: StoryObj<typeof Carousel> = {
83
+ args: { size: 'S' },
84
+ }
85
+
86
+ export const WithGradient: StoryObj<typeof Carousel> = {
87
+ args: { size: 'M', hasGradient: true },
88
+ }
89
+
90
+ export const FullWidth: StoryObj<typeof Carousel> = {
91
+ args: { size: 'M', fullWidth: true },
92
+ }
93
+
94
+ export const NavigationButtonsOnSizeS: StoryObj<typeof Carousel> = {
95
+ args: { size: 'S', navigationButtons: true },
96
+ }
97
+
98
+ export const IndicatorOnSizeM: StoryObj<typeof Carousel> = {
99
+ args: { size: 'M', indicator: true },
100
+ }
101
+
102
+ // 横幅が確定したスロット(番号付き)
103
+ export const DefaultScrollCenter: StoryObj<typeof Carousel> = {
104
+ args: { size: 'M', items: numberedItems, defaultScroll: { align: 'center' } },
105
+ }
106
+
107
+ export const DefaultScrollRight: StoryObj<typeof Carousel> = {
108
+ args: { size: 'M', items: numberedItems, defaultScroll: { align: 'right' } },
109
+ }
110
+
111
+ // 遅延読み込み画像(マウント時に幅が未確定)でも初期位置が効くことの確認
112
+ export const DefaultScrollCenterAsyncImages: StoryObj<typeof Carousel> = {
113
+ args: { size: 'M', items, defaultScroll: { align: 'center' } },
114
+ }
115
+
116
+ // 白フェードが見えることの確認用(暗色コンテンツなら白フェードが明確に出る)
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
+
138
+ export const GradientOnDarkContent: StoryObj<typeof Carousel> = {
139
+ args: { size: 'M', hasGradient: true, items: darkTiles },
140
+ }
141
+
142
+ // scrollStep に関数を渡してフル制御する例(送り量を clientWidth - 48px に固定)。
143
+ // Controls の数値スライダーでは表現できない使い方の確認用。
144
+ export const ScrollStepFunction: StoryObj<typeof Carousel> = {
145
+ args: {
146
+ size: 'M',
147
+ items: numberedItems,
148
+ scrollStep: ({ clientWidth }) => clientWidth - 48,
149
+ },
150
+ }
151
+
152
+ // scroll-snap を item ごと(mandatory)に。align: start で各アイテムが左端にスナップする。
153
+ export const ScrollSnapPerItem: StoryObj<typeof Carousel> = {
154
+ args: {
155
+ size: 'M',
156
+ items: numberedItems,
157
+ scrollSnap: { type: 'mandatory', align: 'start' },
158
+ },
159
+ }
160
+
161
+ export const AllControls: StoryObj<typeof Carousel> = {
162
+ args: {
163
+ size: 'M',
164
+ hasGradient: true,
165
+ fullWidth: false,
166
+ navigationButtons: true,
167
+ indicator: true,
168
+ },
169
+ }