@cakejp/design-system 0.21.0 → 0.21.1
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/CHANGELOG.md +9 -0
- package/contracts/components.json +1 -1
- package/contracts/composition.json +1 -1
- package/contracts/rules.json +1 -1
- package/contracts/screen-patterns.json +1 -1
- package/dist/components/primitives/content-carousel.js +85 -76
- package/dist/index.js +21 -3
- package/dist/types/components/primitives/content-carousel.d.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## [0.21.1] - 2026-10-21
|
|
15
|
+
|
|
16
|
+
### 🐛 Fixed
|
|
17
|
+
|
|
18
|
+
- **`ContentCarousel` の矢印が、複数枚表示(`itemsPerView`)でも 1 枚ずつしか送らなかった**([#325](https://github.com/flashpark/cake-design-system/pull/325)): 見えている枚数を実際の幅から求め、その枚数ずつ送る(PC で 4 枚表示なら 4 枚ずつ)。SP・peek(1 枚表示)は従来どおり 1 枚ずつ。末尾から次へで先頭、先頭から前へで末尾に戻る動きは変えない
|
|
19
|
+
- `autoPlay` も同じく 1 画面ずつ送る。既存の `itemsPerView` 2 以上の利用箇所は、矢印の送り幅が表示枚数に変わる
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
14
23
|
## [0.21.0] - 2026-10-15
|
|
15
24
|
|
|
16
25
|
### ✨ Added
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"name": "Cake.jp Design System — Component Contracts",
|
|
4
|
-
"version": "0.21.
|
|
4
|
+
"version": "0.21.1",
|
|
5
5
|
"description": "全コンポーネントの構造化定義。バリアント・アクセシビリティ要件・使用ルールを機械可読形式で管理。",
|
|
6
6
|
"kind": "kind:\"shell\" は複数ページで再利用されるレイアウト枠(AdminShell / WorkspaceShell / MypageShell 等)。無印は部品コンポーネント。固有ページを組むときは該当 shell から始める。src/components/pages/ 配下のストーリーは特定ページ専用・非配布のため contracts の対象外。",
|
|
7
7
|
"naming": "コンポーネント名は**用途ではなく機能**で付ける。用途名(QuantitySelector = カート数量)にすると、同じ機能を別の文脈で探した人が見つけられず二重実装が生まれる(実例: 用途名だったため NumberInput が別途作られ、意匠の違うステッパーが 2 つになった)。「〜Selector」「〜Picker」等の用途接尾辞を付ける前に、その部品が何をするものかで名前が付けられないかを先に検討する。既存名を機能名へ変える場合は deprecatedAliases に旧名を残す。",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"name": "Cake.jp Design System — Composition Contracts",
|
|
4
|
-
"version": "0.21.
|
|
4
|
+
"version": "0.21.1",
|
|
5
5
|
"description": "ページ骨格・視覚階層・余白リズムの機械可読な正本。画面パターンを選んだ後の「実際の組み方」を規定する。ページの外枠(モバイル/デスクトップ/管理)、商品グリッドの列数、余白の4段リズム、Surface のネスト階層、テキスト階層、CTA の優先順位を構造化する。人間向けの解説は guidelines/composition/layout.md・guidelines/composition/hierarchy.md・guidelines/overview.md。",
|
|
6
6
|
"source": "guidelines/composition/layout.md, guidelines/composition/hierarchy.md, guidelines/overview.md, guidelines/components/button.md(CTA 密度ルール)を正本化",
|
|
7
7
|
"roleSeparation": "screen-patterns.json はページ種別(Standard Page / Focused Workspace / ResponsiveDialog / Dialog)を選ぶための契約。composition.json は選んだ後、その骨格をどう組むか(外枠構造・余白リズム・階層)の契約。前者=何を使うか、後者=どう並べるか。",
|
package/contracts/rules.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"name": "Cake.jp Design System — Screen Patterns Contracts",
|
|
4
|
-
"version": "0.21.
|
|
4
|
+
"version": "0.21.1",
|
|
5
5
|
"description": "CRUD 機能でどの画面パターンを使うかの機械可読な判断基準。5パターンの定義・CRUD 対応・決定木・編集UIの観点比較を構造化する。人間向けの解説は guidelines/foundations/screen-patterns.md。",
|
|
6
6
|
"source": "GitHub issue #37 CRUDの画面設計の型化と基本パターンの整備",
|
|
7
7
|
"relatedContract": "本契約はページ種別の選択(何を使うか)を担う。選んだ後の骨格の組み方(外枠構造・余白リズム・視覚階層)は contracts/composition.json を参照する。",
|
|
@@ -1,156 +1,165 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsxs as w, jsx as
|
|
2
|
+
import { jsxs as w, jsx as n, Fragment as j } from "react/jsx-runtime";
|
|
3
3
|
import * as s from "react";
|
|
4
4
|
import O from "iconsax-reactjs/dist/esm/ArrowLeft2.js";
|
|
5
5
|
import R from "iconsax-reactjs/dist/esm/ArrowRight2.js";
|
|
6
|
-
import { cn as
|
|
6
|
+
import { cn as f } from "../../lib/utils.js";
|
|
7
7
|
import { usePrefersReducedMotion as F } from "../../lib/use-prefers-reduced-motion.js";
|
|
8
|
-
const
|
|
8
|
+
const W = {
|
|
9
9
|
1: "@[960px]:w-full",
|
|
10
10
|
2: "@[960px]:w-[calc((100%-16px)/2)]",
|
|
11
11
|
3: "@[960px]:w-[calc((100%-32px)/3)]",
|
|
12
12
|
4: "@[960px]:w-[calc((100%-48px)/4)]"
|
|
13
13
|
};
|
|
14
|
-
function
|
|
14
|
+
function _({
|
|
15
15
|
slides: p,
|
|
16
16
|
showDots: E = !0,
|
|
17
|
-
showArrows:
|
|
18
|
-
autoPlay:
|
|
19
|
-
peek:
|
|
20
|
-
itemsPerView:
|
|
21
|
-
ariaLabel:
|
|
22
|
-
className:
|
|
23
|
-
...
|
|
17
|
+
showArrows: I = !0,
|
|
18
|
+
autoPlay: m = 0,
|
|
19
|
+
peek: v = !1,
|
|
20
|
+
itemsPerView: b = 1,
|
|
21
|
+
ariaLabel: N = "コンテンツカルーセル",
|
|
22
|
+
className: S,
|
|
23
|
+
...L
|
|
24
24
|
}) {
|
|
25
|
-
const
|
|
25
|
+
const i = s.useRef(null), [c, z] = s.useState(0), l = F(), r = p.length;
|
|
26
26
|
s.useEffect(() => {
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
29
|
-
let
|
|
27
|
+
const e = i.current;
|
|
28
|
+
if (!e) return;
|
|
29
|
+
let t = 0;
|
|
30
30
|
const o = () => {
|
|
31
|
-
cancelAnimationFrame(
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
),
|
|
35
|
-
let
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
}), z(
|
|
31
|
+
cancelAnimationFrame(t), t = requestAnimationFrame(() => {
|
|
32
|
+
const u = Array.from(
|
|
33
|
+
e.querySelectorAll("[data-slide]")
|
|
34
|
+
), M = u[0]?.offsetLeft ?? 0;
|
|
35
|
+
let g = 0, y = Number.POSITIVE_INFINITY;
|
|
36
|
+
u.forEach((k, T) => {
|
|
37
|
+
const C = Math.abs(k.offsetLeft - M - e.scrollLeft);
|
|
38
|
+
C < y && (y = C, g = T);
|
|
39
|
+
}), z(g);
|
|
40
40
|
});
|
|
41
41
|
};
|
|
42
|
-
o(),
|
|
43
|
-
const
|
|
44
|
-
return
|
|
45
|
-
cancelAnimationFrame(
|
|
42
|
+
o(), e.addEventListener("scroll", o, { passive: !0 });
|
|
43
|
+
const a = typeof ResizeObserver > "u" ? void 0 : new ResizeObserver(o);
|
|
44
|
+
return a?.observe(e), () => {
|
|
45
|
+
cancelAnimationFrame(t), e.removeEventListener("scroll", o), a?.disconnect();
|
|
46
46
|
};
|
|
47
|
-
}, [
|
|
47
|
+
}, [r]);
|
|
48
48
|
const d = s.useCallback(
|
|
49
|
-
(
|
|
50
|
-
const
|
|
51
|
-
if (!
|
|
52
|
-
const o =
|
|
53
|
-
o &&
|
|
54
|
-
left: o.offsetLeft -
|
|
55
|
-
behavior:
|
|
49
|
+
(e) => {
|
|
50
|
+
const t = i.current;
|
|
51
|
+
if (!t) return;
|
|
52
|
+
const o = t.children[e], a = t.children[0];
|
|
53
|
+
o && a && t.scrollTo({
|
|
54
|
+
left: o.offsetLeft - a.offsetLeft,
|
|
55
|
+
behavior: l ? "auto" : "smooth"
|
|
56
56
|
});
|
|
57
57
|
},
|
|
58
|
-
[
|
|
59
|
-
),
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
[l]
|
|
59
|
+
), h = s.useCallback(() => {
|
|
60
|
+
const e = i.current, t = e?.children[0], o = e?.children[1];
|
|
61
|
+
if (!e || !t || !o) return 1;
|
|
62
|
+
const a = o.offsetLeft - t.offsetLeft;
|
|
63
|
+
if (a <= 0) return 1;
|
|
64
|
+
const u = a - t.offsetWidth;
|
|
65
|
+
return Math.max(1, Math.floor((e.clientWidth + u + 1) / a));
|
|
66
|
+
}, []), x = s.useCallback(() => {
|
|
67
|
+
const e = i.current, t = e ? e.scrollLeft >= e.scrollWidth - e.clientWidth - 1 : !1;
|
|
68
|
+
d(
|
|
69
|
+
c >= r - 1 || t ? 0 : Math.min(c + h(), r - 1)
|
|
70
|
+
);
|
|
71
|
+
}, [c, r, d, h]);
|
|
63
72
|
s.useEffect(() => {
|
|
64
|
-
if (
|
|
65
|
-
const
|
|
66
|
-
return () => clearInterval(
|
|
67
|
-
}, [
|
|
73
|
+
if (l || m <= 0 || r <= 1) return;
|
|
74
|
+
const e = setInterval(x, m);
|
|
75
|
+
return () => clearInterval(e);
|
|
76
|
+
}, [m, r, l, x]);
|
|
68
77
|
function A() {
|
|
69
|
-
d(
|
|
78
|
+
d(c <= 0 ? r - 1 : Math.max(0, c - h()));
|
|
70
79
|
}
|
|
71
|
-
return
|
|
80
|
+
return r === 0 ? null : /* @__PURE__ */ w(
|
|
72
81
|
"div",
|
|
73
82
|
{
|
|
74
83
|
"data-slot": "content-carousel",
|
|
75
84
|
role: "region",
|
|
76
85
|
"aria-roledescription": "carousel",
|
|
77
|
-
"aria-label":
|
|
78
|
-
className:
|
|
79
|
-
...
|
|
86
|
+
"aria-label": N,
|
|
87
|
+
className: f("@container group/carousel relative", S),
|
|
88
|
+
...L,
|
|
80
89
|
children: [
|
|
81
|
-
/* @__PURE__ */
|
|
90
|
+
/* @__PURE__ */ n(
|
|
82
91
|
"div",
|
|
83
92
|
{
|
|
84
|
-
ref:
|
|
85
|
-
className:
|
|
93
|
+
ref: i,
|
|
94
|
+
className: f(
|
|
86
95
|
"flex snap-x snap-mandatory overflow-x-auto scroll-smooth motion-reduce:scroll-auto scrollbar-none",
|
|
87
96
|
// peek: 端の余白 = Container の gutter(外に置いたときは 16px)。Container の中では
|
|
88
97
|
// gutter 分だけ外へはみ出して画面端までスクロールさせる。scroll-padding も同じ幅にして、
|
|
89
98
|
// スナップ後も端の余白が消えないようにする。PC (@[960px]:) では peek しないので外す
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
v && "-mx-[var(--Container-Gutter,0px)] gap-3 px-[var(--Container-Gutter,16px)] scroll-px-[var(--Container-Gutter,16px)] @[960px]:mx-0 @[960px]:gap-0 @[960px]:px-0 @[960px]:scroll-px-0",
|
|
100
|
+
b > 1 && "@[960px]:gap-4"
|
|
92
101
|
),
|
|
93
102
|
style: { scrollbarWidth: "none", msOverflowStyle: "none" },
|
|
94
|
-
children: p.map((
|
|
103
|
+
children: p.map((e, t) => /* @__PURE__ */ n(
|
|
95
104
|
"div",
|
|
96
105
|
{
|
|
97
106
|
"data-slide": !0,
|
|
98
|
-
"data-index":
|
|
107
|
+
"data-index": t,
|
|
99
108
|
role: "group",
|
|
100
109
|
"aria-roledescription": "スライド",
|
|
101
|
-
"aria-label": `${
|
|
102
|
-
className:
|
|
110
|
+
"aria-label": `${t + 1} / ${r}`,
|
|
111
|
+
className: f(
|
|
103
112
|
"shrink-0 snap-start",
|
|
104
113
|
// peek: 端の余白 16px のとき、カード幅は導入前(全幅に置いた場合)と同じ「全幅 - 76px」
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
v ? "w-[calc(100%-44px)]" : "w-full",
|
|
115
|
+
W[b]
|
|
107
116
|
),
|
|
108
|
-
children:
|
|
117
|
+
children: e
|
|
109
118
|
},
|
|
110
|
-
|
|
119
|
+
t
|
|
111
120
|
))
|
|
112
121
|
}
|
|
113
122
|
),
|
|
114
|
-
|
|
115
|
-
/* @__PURE__ */
|
|
123
|
+
I && r > 1 && /* @__PURE__ */ w(j, { children: [
|
|
124
|
+
/* @__PURE__ */ n(
|
|
116
125
|
"button",
|
|
117
126
|
{
|
|
118
127
|
type: "button",
|
|
119
128
|
onClick: A,
|
|
120
129
|
"aria-label": "前へ",
|
|
121
130
|
className: "absolute left-2 top-1/2 z-10 hidden size-10 -translate-y-1/2 items-center justify-center rounded-full border border-[var(--Border-Control,var(--Border-Medium-Emphasis))] bg-[var(--Surface-Secondary)] shadow-md transition-opacity hover:bg-[var(--Surface-Tertiary)] @md:flex @[960px]:opacity-0 @[960px]:group-hover/carousel:opacity-100",
|
|
122
|
-
children: /* @__PURE__ */
|
|
131
|
+
children: /* @__PURE__ */ n(O, { size: 20, className: "text-[var(--Object-High-Emphasis)]" })
|
|
123
132
|
}
|
|
124
133
|
),
|
|
125
|
-
/* @__PURE__ */
|
|
134
|
+
/* @__PURE__ */ n(
|
|
126
135
|
"button",
|
|
127
136
|
{
|
|
128
137
|
type: "button",
|
|
129
|
-
onClick:
|
|
138
|
+
onClick: x,
|
|
130
139
|
"aria-label": "次へ",
|
|
131
140
|
className: "absolute right-2 top-1/2 z-10 hidden size-10 -translate-y-1/2 items-center justify-center rounded-full border border-[var(--Border-Control,var(--Border-Medium-Emphasis))] bg-[var(--Surface-Secondary)] shadow-md transition-opacity hover:bg-[var(--Surface-Tertiary)] @md:flex @[960px]:opacity-0 @[960px]:group-hover/carousel:opacity-100",
|
|
132
|
-
children: /* @__PURE__ */
|
|
141
|
+
children: /* @__PURE__ */ n(R, { size: 20, className: "text-[var(--Object-High-Emphasis)]" })
|
|
133
142
|
}
|
|
134
143
|
)
|
|
135
144
|
] }),
|
|
136
|
-
E &&
|
|
145
|
+
E && r > 1 && /* @__PURE__ */ n("div", { className: "mt-2 flex flex-wrap items-center justify-center gap-1.5 @[960px]:hidden", children: p.map((e, t) => /* @__PURE__ */ n(
|
|
137
146
|
"button",
|
|
138
147
|
{
|
|
139
148
|
type: "button",
|
|
140
|
-
onClick: () => d(
|
|
141
|
-
"aria-label": `スライド ${
|
|
142
|
-
className:
|
|
149
|
+
onClick: () => d(t),
|
|
150
|
+
"aria-label": `スライド ${t + 1}`,
|
|
151
|
+
className: f(
|
|
143
152
|
// shrink-0 必須: 空要素なので min-content 幅が 0、既定の flex-shrink:1 だとドットが消える
|
|
144
153
|
"size-2 shrink-0 rounded-full transition-colors",
|
|
145
|
-
|
|
154
|
+
t === c ? "bg-[var(--Text-High-Emphasis)]" : "bg-[var(--Surface-Tertiary)]"
|
|
146
155
|
)
|
|
147
156
|
},
|
|
148
|
-
|
|
157
|
+
t
|
|
149
158
|
)) })
|
|
150
159
|
]
|
|
151
160
|
}
|
|
152
161
|
);
|
|
153
162
|
}
|
|
154
163
|
export {
|
|
155
|
-
|
|
164
|
+
_ as ContentCarousel
|
|
156
165
|
};
|
package/dist/index.js
CHANGED
|
@@ -16700,6 +16700,7 @@ export {
|
|
|
16700
16700
|
// /**
|
|
16701
16701
|
// * PC(@[960px]:)で同時に見せるカード数。SP は従来どおり1枚表示。
|
|
16702
16702
|
// * 求人カード等、EC 商品以外の複数カードを横スクロールさせる用途に使う。
|
|
16703
|
+
// * 矢印(と autoPlay)は見えている枚数ずつ送る(PC で 4 枚表示なら 4 枚ずつ)。
|
|
16703
16704
|
// */
|
|
16704
16705
|
// itemsPerView?: ContentCarouselItemsPerView
|
|
16705
16706
|
// /** カルーセル全体の aria-label(スクリーンリーダー向け説明) */
|
|
@@ -16811,6 +16812,19 @@ export {
|
|
|
16811
16812
|
// [reduceMotion]
|
|
16812
16813
|
// )
|
|
16813
16814
|
//
|
|
16815
|
+
// // 見えているスライドの枚数(矢印はこの枚数ずつ送る)。PC の複数枚表示では itemsPerView 枚、
|
|
16816
|
+
// // SP・peek では 1 枚。幅の切り替えは container query なので、props ではなく実際の幅から求める
|
|
16817
|
+
// const visibleCount = React.useCallback(() => {
|
|
16818
|
+
// const container = scrollRef.current
|
|
16819
|
+
// const first = container?.children[0] as HTMLElement | undefined
|
|
16820
|
+
// const second = container?.children[1] as HTMLElement | undefined
|
|
16821
|
+
// if (!container || !first || !second) return 1
|
|
16822
|
+
// const stride = second.offsetLeft - first.offsetLeft
|
|
16823
|
+
// if (stride <= 0) return 1
|
|
16824
|
+
// const gap = stride - first.offsetWidth
|
|
16825
|
+
// return Math.max(1, Math.floor((container.clientWidth + gap + 1) / stride))
|
|
16826
|
+
// }, [])
|
|
16827
|
+
//
|
|
16814
16828
|
// // 次へ。末尾までスクロールしきっていたら先頭に戻る。複数枚表示・peek では最後のスライドを先頭に
|
|
16815
16829
|
// // そろえられない(スクロール量が足りない)ため、現在位置が最後でなくても末尾で止まり続けないようにする
|
|
16816
16830
|
// const next = React.useCallback(() => {
|
|
@@ -16818,8 +16832,12 @@ export {
|
|
|
16818
16832
|
// const atEnd = container
|
|
16819
16833
|
// ? container.scrollLeft >= container.scrollWidth - container.clientWidth - 1
|
|
16820
16834
|
// : false
|
|
16821
|
-
// scrollTo(
|
|
16822
|
-
//
|
|
16835
|
+
// scrollTo(
|
|
16836
|
+
// activeIndex >= totalSlides - 1 || atEnd
|
|
16837
|
+
// ? 0
|
|
16838
|
+
// : Math.min(activeIndex + visibleCount(), totalSlides - 1)
|
|
16839
|
+
// )
|
|
16840
|
+
// }, [activeIndex, totalSlides, scrollTo, visibleCount])
|
|
16823
16841
|
//
|
|
16824
16842
|
// // Auto play(reduced-motion のときは始めない。停止ボタンは持たないため、勝手に動かさないことで配慮する)
|
|
16825
16843
|
// React.useEffect(() => {
|
|
@@ -16829,7 +16847,7 @@ export {
|
|
|
16829
16847
|
// }, [autoPlay, totalSlides, reduceMotion, next])
|
|
16830
16848
|
//
|
|
16831
16849
|
// function prev() {
|
|
16832
|
-
// scrollTo(activeIndex <= 0 ? totalSlides - 1 : activeIndex -
|
|
16850
|
+
// scrollTo(activeIndex <= 0 ? totalSlides - 1 : Math.max(0, activeIndex - visibleCount()))
|
|
16833
16851
|
// }
|
|
16834
16852
|
//
|
|
16835
16853
|
// if (totalSlides === 0) return null
|
|
@@ -32,6 +32,7 @@ export interface ContentCarouselProps extends Omit<React.HTMLAttributes<HTMLDivE
|
|
|
32
32
|
/**
|
|
33
33
|
* PC(@[960px]:)で同時に見せるカード数。SP は従来どおり1枚表示。
|
|
34
34
|
* 求人カード等、EC 商品以外の複数カードを横スクロールさせる用途に使う。
|
|
35
|
+
* 矢印(と autoPlay)は見えている枚数ずつ送る(PC で 4 枚表示なら 4 枚ずつ)。
|
|
35
36
|
*/
|
|
36
37
|
itemsPerView?: ContentCarouselItemsPerView;
|
|
37
38
|
/** カルーセル全体の aria-label(スクリーンリーダー向け説明) */
|