@yadeck/slide-components-basic 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +16 -2
- package/dist/chrome.d.ts +8 -0
- package/dist/chrome.d.ts.map +1 -0
- package/dist/chrome.js +17 -0
- package/dist/chrome.js.map +1 -0
- package/dist/flow.d.ts +26 -0
- package/dist/flow.d.ts.map +1 -0
- package/dist/flow.js +46 -0
- package/dist/flow.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/layouts.d.ts +114 -0
- package/dist/layouts.d.ts.map +1 -0
- package/dist/layouts.js +220 -0
- package/dist/layouts.js.map +1 -0
- package/dist/panes.d.ts +43 -0
- package/dist/panes.d.ts.map +1 -0
- package/dist/panes.js +131 -0
- package/dist/panes.js.map +1 -0
- package/dist/styles.d.ts +12 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +157 -0
- package/dist/styles.js.map +1 -0
- package/package.json +60 -4
- package/src/chrome.tsx +29 -0
- package/src/flow.tsx +89 -0
- package/src/index.ts +91 -0
- package/src/layouts.tsx +508 -0
- package/src/panes.tsx +213 -0
- package/src/styles.ts +157 -0
package/src/panes.tsx
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { Block, RichText, pick, type BlockNode, type SlideNode } from "@yadeck/slide-core";
|
|
2
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
3
|
+
import { PAGE, SlideHeading } from "./chrome.js";
|
|
4
|
+
import { codeChrome, codeSize } from "./layouts.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* tags 無しの slide の自動レイアウト。
|
|
8
|
+
* 内容を pane に数え、数に応じて配置を決める。
|
|
9
|
+
* - テキスト(body / list / tags 無し / その他の block)は全部まとめて 1 pane
|
|
10
|
+
* - image / code / video は 1 つにつき 1 pane
|
|
11
|
+
* 1: 全面、2: 左右、3: ト型(左 1 + 右 2)、4: 十字、5 以上: 左 floor(N/2) + 右 残り(8 まで)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const SLIDE_W = 1280;
|
|
15
|
+
const SLIDE_H = 720;
|
|
16
|
+
const PAD_X = 80;
|
|
17
|
+
const PAD_Y = 56;
|
|
18
|
+
const HEADING_H = 69 + 24;
|
|
19
|
+
const DESCRIPTION_H = 36;
|
|
20
|
+
const GAP = 32;
|
|
21
|
+
|
|
22
|
+
/** 1 つで 1 pane になる block の tags */
|
|
23
|
+
const PANE_TAGS = ["image", "code", "video", "flow"];
|
|
24
|
+
|
|
25
|
+
type Pane = { kind: "text"; blocks: BlockNode[] } | { kind: "block"; block: BlockNode };
|
|
26
|
+
|
|
27
|
+
function isPaneBlock(b: BlockNode): boolean {
|
|
28
|
+
return b.tags.some((t) => PANE_TAGS.includes(t));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 出現順を保ったまま pane に分ける。テキスト pane は最初のテキスト block の位置に置く */
|
|
32
|
+
export function toPanes(blocks: BlockNode[]): Pane[] {
|
|
33
|
+
const panes: Pane[] = [];
|
|
34
|
+
let text: Extract<Pane, { kind: "text" }> | undefined;
|
|
35
|
+
for (const b of blocks) {
|
|
36
|
+
if (b.tags.includes("note")) continue;
|
|
37
|
+
if (isPaneBlock(b)) {
|
|
38
|
+
panes.push({ kind: "block", block: b });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!text) {
|
|
42
|
+
text = { kind: "text", blocks: [] };
|
|
43
|
+
panes.push(text);
|
|
44
|
+
}
|
|
45
|
+
text.blocks.push(b);
|
|
46
|
+
}
|
|
47
|
+
return panes;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** pane を左右の列に振り分ける。戻り値は [左の pane 数, 右の pane 数] */
|
|
51
|
+
export function splitColumns(n: number): [number, number] {
|
|
52
|
+
if (n <= 1) return [n, 0];
|
|
53
|
+
const left = Math.floor(n / 2);
|
|
54
|
+
return [left, n - left];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface PaneBox {
|
|
58
|
+
w: number;
|
|
59
|
+
h: number;
|
|
60
|
+
/** 縦に積んだときの高さの比(flex-grow) */
|
|
61
|
+
grow?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 同じ slide に code が 2 つ以上あるとき、全部が収まる最小のフォントサイズを --code-fit で渡す。
|
|
67
|
+
* pane ごとに最大サイズを選ぶと、並んだコードの文字の大きさが揃わないため。式は styles.ts の .b-code と同じ
|
|
68
|
+
*/
|
|
69
|
+
export function codeFitStyle(placed: { pane: Pane; box: PaneBox }[]): CSSProperties {
|
|
70
|
+
const terms = placed.flatMap(({ pane, box }) => {
|
|
71
|
+
if (pane.kind !== "block" || !pane.block.tags.includes("code")) return [];
|
|
72
|
+
const { cols, rows } = codeSize(pane.block.text);
|
|
73
|
+
const chrome = codeChrome(typeof pane.block.caption === "string" ? pane.block.caption : undefined);
|
|
74
|
+
return [`calc((${Math.round(box.w)}px - 48px) / ${cols} / 0.62)`, `calc((${Math.round(box.h)}px - ${chrome}px) / ${rows} / 1.375)`];
|
|
75
|
+
});
|
|
76
|
+
return terms.length >= 4 ? ({ "--code-fit": `min(${terms.join(", ")})` } as CSSProperties) : {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 縦に積む pane の高さの比。全部が code なら「行数 + caption などの余白」の比で分け、
|
|
81
|
+
* 行の多いコードが切れたり、短いコードの箱が間延びしたりしないようにする。それ以外は等分
|
|
82
|
+
*/
|
|
83
|
+
export function paneWeights(panes: Pane[]): number[] {
|
|
84
|
+
const sizes = panes.map((p) =>
|
|
85
|
+
p.kind === "block" && p.block.tags.includes("code")
|
|
86
|
+
? codeSize(p.block.text).rows + codeChrome(typeof p.block.caption === "string" ? p.block.caption : undefined) / 24
|
|
87
|
+
: undefined,
|
|
88
|
+
);
|
|
89
|
+
return sizes.every((x) => x !== undefined) ? (sizes as number[]) : panes.map(() => 1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 1 列に縦に積んだ pane の寸法 */
|
|
93
|
+
function columnBoxes(contentH: number, columnCount: number, column: Pane[]): PaneBox[] {
|
|
94
|
+
const w = columnCount === 1 ? SLIDE_W - PAD_X * 2 : (SLIDE_W - PAD_X * 2 - GAP) / 2;
|
|
95
|
+
const weights = paneWeights(column);
|
|
96
|
+
const total = weights.reduce((a, b) => a + b, 0);
|
|
97
|
+
const free = contentH - GAP * (column.length - 1);
|
|
98
|
+
return weights.map((wt) => ({ w, h: (free * wt) / total, grow: wt }));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** pane の寸法を CSS 変数で渡す。code の自動フォントサイズなどが参照する */
|
|
102
|
+
const PaneFrame = ({ box, children }: { box: PaneBox; children: ReactNode }) => (
|
|
103
|
+
<div
|
|
104
|
+
className="b-pane relative flex min-h-0 min-w-0 flex-1 flex-col"
|
|
105
|
+
style={{ "--pane-w": `${Math.round(box.w)}px`, "--pane-h": `${Math.round(box.h)}px`, flexGrow: box.grow ?? 1, flexBasis: 0 } as CSSProperties}
|
|
106
|
+
>
|
|
107
|
+
{children}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
/** テキスト pane。body / list / tags 無しの block は行にまとめ、1 行なら中央に大きく、2 行以上なら箇条書きにする */
|
|
112
|
+
export const TextPane = ({ blocks }: { blocks: BlockNode[] }) => {
|
|
113
|
+
const lines: ReactNode[] = [];
|
|
114
|
+
const others: BlockNode[] = [];
|
|
115
|
+
for (const b of blocks) {
|
|
116
|
+
const isText = b.tags.length === 0 || b.tags.includes("body") || b.tags.includes("list");
|
|
117
|
+
if (!isText) {
|
|
118
|
+
others.push(b);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
for (const line of (b.text ?? "").split("\n")) {
|
|
122
|
+
if (line.trim()) lines.push(<RichText key={`${b.id}-${lines.length}`} text={line} />);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (lines.length === 1 && others.length === 0) {
|
|
126
|
+
return (
|
|
127
|
+
<div className="b-center flex flex-1 items-center justify-center text-center text-h3 leading-relaxed">
|
|
128
|
+
{lines[0]}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return (
|
|
133
|
+
<div className="b-bullets flex flex-1 flex-col justify-center gap-5">
|
|
134
|
+
{lines.length > 0 && (
|
|
135
|
+
<ul className="m-0 list-disc pl-[1.2em] marker:text-accent">
|
|
136
|
+
{lines.map((l, i) => (
|
|
137
|
+
<li key={i} className="my-2">
|
|
138
|
+
{l}
|
|
139
|
+
</li>
|
|
140
|
+
))}
|
|
141
|
+
</ul>
|
|
142
|
+
)}
|
|
143
|
+
{others.map((b) => (
|
|
144
|
+
<Block key={b.id} data={b} />
|
|
145
|
+
))}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const PaneView = ({ pane, box }: { pane: Pane; box: PaneBox }) => (
|
|
151
|
+
<PaneFrame box={box}>{pane.kind === "text" ? <TextPane blocks={pane.blocks} /> : <Block data={pane.block} />}</PaneFrame>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
export const DefaultSlide = (slide: SlideNode) => {
|
|
155
|
+
const [heading, rest] = pick(slide.items, ["heading"]);
|
|
156
|
+
const panes = toPanes(rest);
|
|
157
|
+
const contentH = SLIDE_H - PAD_Y * 2 - (heading ? HEADING_H : 0) - (heading && slide.description ? DESCRIPTION_H : 0);
|
|
158
|
+
const [leftCount, rightCount] = splitColumns(panes.length);
|
|
159
|
+
const left = panes.slice(0, leftCount);
|
|
160
|
+
const right = panes.slice(leftCount);
|
|
161
|
+
const columns = panes.length <= 1 ? [panes] : [left, right];
|
|
162
|
+
const boxes = columns.map((column) => columnBoxes(contentH, columns.length, column));
|
|
163
|
+
const placed = columns.flatMap((column, ci) => column.map((pane, i) => ({ pane, box: boxes[ci]![i]! })));
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div className={`${PAGE} b-default`} style={codeFitStyle(placed)}>
|
|
167
|
+
{heading && <SlideHeading block={heading} description={slide.description} />}
|
|
168
|
+
{panes.length <= 1 ? (
|
|
169
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
170
|
+
{panes[0] && <PaneView pane={panes[0]} box={boxes[0]![0]!} />}
|
|
171
|
+
</div>
|
|
172
|
+
) : (
|
|
173
|
+
<div className="grid min-h-0 flex-1 grid-cols-2 gap-8">
|
|
174
|
+
{columns.map((column, ci) => (
|
|
175
|
+
<div key={ci} className="flex min-h-0 min-w-0 flex-col gap-8">
|
|
176
|
+
{column.map((pane, i) => (
|
|
177
|
+
<PaneView key={i} pane={pane} box={boxes[ci]![i]!} />
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** columns の列数。4 までは横一列、5 以上は 3 列で折り返す */
|
|
188
|
+
export function columnsFor(n: number): number {
|
|
189
|
+
return n <= 4 ? Math.max(1, n) : 3;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** `columns` を付けた slide。pane を横に等分し、5 以上は 3 列 × 2 行に折り返す */
|
|
193
|
+
export const ColumnsSlide = (slide: SlideNode) => {
|
|
194
|
+
const [heading, rest] = pick(slide.items, ["heading"]);
|
|
195
|
+
const panes = toPanes(rest);
|
|
196
|
+
const contentH = SLIDE_H - PAD_Y * 2 - (heading ? HEADING_H : 0) - (heading && slide.description ? DESCRIPTION_H : 0);
|
|
197
|
+
const cols = columnsFor(panes.length);
|
|
198
|
+
const rows = Math.max(1, Math.ceil(panes.length / cols));
|
|
199
|
+
const box: PaneBox = { w: (SLIDE_W - PAD_X * 2 - GAP * (cols - 1)) / cols, h: (contentH - GAP * (rows - 1)) / rows };
|
|
200
|
+
return (
|
|
201
|
+
<div className={`${PAGE} b-columns`} style={codeFitStyle(panes.map((pane) => ({ pane, box })))}>
|
|
202
|
+
{heading && <SlideHeading block={heading} description={slide.description} />}
|
|
203
|
+
<div
|
|
204
|
+
className="grid min-h-0 flex-1 gap-8"
|
|
205
|
+
style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`, gridTemplateRows: `repeat(${rows}, minmax(0, 1fr))` }}
|
|
206
|
+
>
|
|
207
|
+
{panes.map((pane, i) => (
|
|
208
|
+
<PaneView key={i} pane={pane} box={box} />
|
|
209
|
+
))}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
};
|
package/src/styles.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* basic の既定テーマ。Tailwind v4 の `@theme` トークンで、利用者は slide.config.ts の `theme` に
|
|
3
|
+
* 指定した CSS で同名トークンを上書きして色・フォント・文字サイズを差し替える。
|
|
4
|
+
* 色は `bg-primary` `text-ink`、フォントは `font-sans`、サイズは `text-h1` のように utility になる。
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_THEME = `
|
|
7
|
+
@theme {
|
|
8
|
+
--color-primary: #2a5bd7;
|
|
9
|
+
--color-primary-deep: #163a8f;
|
|
10
|
+
--color-accent: #e8c547;
|
|
11
|
+
--color-ink: #222222;
|
|
12
|
+
--color-muted: #666666;
|
|
13
|
+
--color-faint: #888888;
|
|
14
|
+
--color-surface: #ffffff;
|
|
15
|
+
--color-code: #1e1e1e;
|
|
16
|
+
--color-code-ink: #eeeeee;
|
|
17
|
+
|
|
18
|
+
--font-sans: "Helvetica Neue", Arial, "Hiragino Sans", "Noto Sans JP", sans-serif;
|
|
19
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
20
|
+
|
|
21
|
+
--text-body: 28px;
|
|
22
|
+
--text-small: 22px;
|
|
23
|
+
--text-h1: 64px;
|
|
24
|
+
--text-h2: 44px;
|
|
25
|
+
--text-h3: 34px;
|
|
26
|
+
--text-h4: 30px;
|
|
27
|
+
--text-display: 96px;
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* utility を付けられない要素(RichText が生成する markdown 由来の p / ul / code など)のための最小 CSS。
|
|
33
|
+
* Layout 自体の見た目は layouts.tsx の className で持つ。
|
|
34
|
+
*/
|
|
35
|
+
export const BASIC_CSS = `
|
|
36
|
+
@layer components {
|
|
37
|
+
.yd-slide { font-family: var(--font-sans); color: var(--color-ink); font-size: var(--text-body); line-height: 1.5; }
|
|
38
|
+
.b-body p { margin: 0 0 .6em; }
|
|
39
|
+
.b-body p:last-child { margin-bottom: 0; }
|
|
40
|
+
.b-body ul { list-style: disc; padding-left: 1.2em; }
|
|
41
|
+
.b-body ol { list-style: decimal; padding-left: 1.2em; }
|
|
42
|
+
.b-body li::marker { color: var(--color-accent); }
|
|
43
|
+
/* 本文中のコード。合字(-> が → になる等)は原文と違って見えるので切る */
|
|
44
|
+
.yd-slide :not(pre) > code { font-family: var(--font-mono); font-size: .9em; font-variant-ligatures: none; background: color-mix(in srgb, var(--color-ink) 8%, transparent); padding: 0 .3em; border-radius: 4px; }
|
|
45
|
+
.b-body a { color: var(--color-primary); text-decoration: underline; }
|
|
46
|
+
|
|
47
|
+
/* code: pane の寸法(--pane-w / --pane-h)と行数・最大行長から、収まる最大のフォントサイズを選ぶ */
|
|
48
|
+
.yd-slide { --pane-w: 1120px; --pane-h: 500px; }
|
|
49
|
+
.b-code {
|
|
50
|
+
font-size: max(14px, min(
|
|
51
|
+
var(--text-body),
|
|
52
|
+
calc((var(--pane-w) - 48px) / var(--code-cols, 40) / 0.62),
|
|
53
|
+
calc((var(--pane-h) - var(--code-chrome, 40px)) / var(--code-rows, 10) / 1.375),
|
|
54
|
+
var(--code-fit, 9999px)
|
|
55
|
+
));
|
|
56
|
+
/* 下限まで縮めても入らない長い行は折り返す。はみ出して読めないよりよい */
|
|
57
|
+
white-space: pre-wrap;
|
|
58
|
+
overflow-wrap: anywhere;
|
|
59
|
+
font-variant-ligatures: none;
|
|
60
|
+
}
|
|
61
|
+
.b-code .hljs { color: var(--color-code-ink); }
|
|
62
|
+
.hljs-comment, .hljs-quote { color: #8b949e; font-style: italic; }
|
|
63
|
+
.hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-type, .hljs-built_in, .hljs-doctag, .hljs-meta { color: #ff7b72; }
|
|
64
|
+
.hljs-string, .hljs-regexp, .hljs-addition, .hljs-attr, .hljs-meta .hljs-string { color: #a5d6ff; }
|
|
65
|
+
.hljs-number, .hljs-symbol, .hljs-bullet, .hljs-link, .hljs-variable, .hljs-template-variable { color: #79c0ff; }
|
|
66
|
+
.hljs-title, .hljs-section, .hljs-name, .hljs-selector-id, .hljs-selector-class, .hljs-function .hljs-title { color: #d2a8ff; }
|
|
67
|
+
.hljs-attribute, .hljs-params, .hljs-property { color: #ffa657; }
|
|
68
|
+
.hljs-emphasis { font-style: italic; }
|
|
69
|
+
.hljs-strong { font-weight: bold; }
|
|
70
|
+
.hljs-deletion { color: #ffa198; }
|
|
71
|
+
|
|
72
|
+
/* ---- flow(手順)。https://www.ameamelog.com/css-flow/ の 5 パターンを移植。寸法はスライド向けに 1.5 倍 ---- */
|
|
73
|
+
.b-flow { --flow-grad: linear-gradient(to right, var(--color-primary) 0%, color-mix(in srgb, var(--color-primary) 55%, white) 100%); margin: auto 0; padding: 0; list-style: none; width: 100%; }
|
|
74
|
+
.b-flow > li { list-style: none; }
|
|
75
|
+
.b-flow dl, .b-flow dd { margin: 0; }
|
|
76
|
+
.b-flow dt { font-size: var(--text-h4); font-weight: 700; color: var(--color-primary); line-height: 1.3; }
|
|
77
|
+
.b-flow dd { font-size: var(--text-small); color: color-mix(in srgb, var(--color-ink) 80%, transparent); line-height: 1.45; }
|
|
78
|
+
.b-flow .phase { display: inline-block; margin-left: .6em; padding: .1em .7em; border-radius: 999px; background: color-mix(in srgb, var(--color-primary) 10%, transparent); font-size: var(--text-small); font-weight: 700; color: var(--color-primary); vertical-align: middle; }
|
|
79
|
+
.b-flow .via { font-family: var(--font-mono); font-size: var(--text-small); font-weight: 700; color: var(--color-primary); white-space: nowrap; }
|
|
80
|
+
.b-flow .icon { font-size: 18px; font-weight: 700; color: #fff; background: var(--flow-grad); }
|
|
81
|
+
.b-flow.has-focus > li:not(.is-focus) { opacity: .35; }
|
|
82
|
+
.b-flow > li.is-focus .icon { background: var(--color-accent); color: var(--color-ink); }
|
|
83
|
+
.b-flow > li.is-focus dt { color: var(--color-ink); }
|
|
84
|
+
|
|
85
|
+
/* 1 カラム構成: 左に STEP ピル、縦レール、点と点線、右に見出しと説明 */
|
|
86
|
+
.b-flow-rail { padding-left: 180px; position: relative; }
|
|
87
|
+
.b-flow-rail::before { content: ""; width: 22px; height: 100%; background: color-mix(in srgb, var(--color-ink) 7%, transparent); margin-left: -11px; display: block; position: absolute; top: 0; left: 180px; }
|
|
88
|
+
.b-flow-rail > li { position: relative; }
|
|
89
|
+
.b-flow-rail > li:not(:last-child) { margin-bottom: 60px; }
|
|
90
|
+
.b-flow-rail > li .icon { padding: 10px 28px; display: block; position: absolute; top: 0; left: -180px; z-index: 2; }
|
|
91
|
+
.b-flow-rail > li .icon::after { content: ""; border-style: solid; border-width: 8px 0 8px 14px; border-color: transparent transparent transparent color-mix(in srgb, var(--color-primary) 55%, white); position: absolute; top: 50%; left: 100%; transform: translateY(-50%); }
|
|
92
|
+
.b-flow-rail > li.is-focus .icon::after { border-left-color: var(--color-accent); }
|
|
93
|
+
.b-flow-rail > li .via { position: absolute; top: -30px; left: 28px; transform: translateY(-50%); }
|
|
94
|
+
.b-flow-rail > li dl { padding-left: 100px; position: relative; }
|
|
95
|
+
.b-flow-rail > li dl::before, .b-flow-rail > li dl::after { content: ""; display: block; position: absolute; top: 24px; }
|
|
96
|
+
.b-flow-rail > li dl::before { width: 11px; height: 11px; margin-top: -5px; background: var(--color-primary); border-radius: 50%; left: -6px; z-index: 1; }
|
|
97
|
+
.b-flow-rail > li.is-focus dl::before { background: var(--color-accent); }
|
|
98
|
+
.b-flow-rail > li dl::after { width: 72px; border-bottom: 2px dashed color-mix(in srgb, var(--color-ink) 40%, transparent); left: 8px; }
|
|
99
|
+
.b-flow-rail > li dl dt { margin-bottom: 6px; }
|
|
100
|
+
|
|
101
|
+
/* ポップ: 吹き出しの番号 + 枠カードを横に並べ、間に › */
|
|
102
|
+
.b-flow-pop { --flow-gap: 56px; display: flex; justify-content: space-between; gap: var(--flow-gap); }
|
|
103
|
+
.b-flow-pop > li { flex: 1 1 0; min-width: 0; position: relative; }
|
|
104
|
+
.b-flow-pop > li .icon { font-family: var(--font-mono); font-size: 26px; line-height: 72px; width: 72px; background: var(--color-primary); margin: 0 auto 28px; display: block; border-radius: 50%; text-align: center; position: relative; letter-spacing: 2px; }
|
|
105
|
+
.b-flow-pop > li .icon::before { content: ""; border: solid transparent; border-width: 12px; border-top-color: var(--color-primary); position: absolute; top: calc(100% - 3px); left: 50%; transform: translateX(-50%); }
|
|
106
|
+
.b-flow-pop > li.is-focus .icon::before { border-top-color: var(--color-accent); }
|
|
107
|
+
.b-flow-pop > li .via { position: absolute; top: 24px; right: calc(100% + var(--flow-gap) / 2 + 4px); transform: translateX(50%); }
|
|
108
|
+
.b-flow-pop > li dl { padding: 32px 10px; word-break: auto-phrase; text-wrap: balance; border: 4px solid var(--color-primary); position: relative; text-align: center; height: calc(100% - 100px); }
|
|
109
|
+
.b-flow-pop > li.is-focus dl { border-color: var(--color-accent); }
|
|
110
|
+
/* 箱の間の ›。隙間(--flow-gap)の中央に置き、枠線に重ねない */
|
|
111
|
+
.b-flow-pop > li:not(:last-child) dl::before { content: ""; width: 36px; height: 36px; display: block; border-top: 8px solid var(--color-primary); border-right: 8px solid var(--color-primary); position: absolute; top: 50%; left: calc(100% + var(--flow-gap) / 2 - 8px); transform: translate(-50%, -50%) rotate(45deg); }
|
|
112
|
+
.b-flow-pop > li dl dt { margin-bottom: 10px; word-break: keep-all; }
|
|
113
|
+
.b-flow-pop > li dl dt .phase { display: block; margin: 8px auto 0; padding: .15em .6em; font-size: 18px; width: fit-content; max-width: 100%; box-sizing: border-box; white-space: nowrap; }
|
|
114
|
+
|
|
115
|
+
/* 枠線を使ったデザイン: 角丸の枠を縦に積み、点線と ▽ でつなぐ */
|
|
116
|
+
.b-flow-boxed > li { position: relative; }
|
|
117
|
+
.b-flow-boxed > li:not(:last-child) { margin-bottom: 40px; }
|
|
118
|
+
.b-flow-boxed > li:not(:first-child)::before { content: ""; height: 60px; display: block; border-left: 5px dotted color-mix(in srgb, var(--color-ink) 15%, transparent); position: absolute; top: -40px; left: calc(12% + 40px); z-index: 1; }
|
|
119
|
+
.b-flow-boxed > li .via { position: absolute; top: -20px; left: calc(12% + 72px); transform: translateY(-50%); }
|
|
120
|
+
.b-flow-boxed > li dl { width: 100%; padding: 8px 36px; display: flex; align-items: center; border: 3px solid var(--color-primary); border-radius: 14px; position: relative; background: var(--color-surface); }
|
|
121
|
+
.b-flow-boxed > li.is-focus dl { border-color: var(--color-accent); }
|
|
122
|
+
.b-flow-boxed > li:not(:last-child) dl::before, .b-flow-boxed > li:not(:last-child) dl::after { content: ""; border: solid transparent; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); }
|
|
123
|
+
.b-flow-boxed > li:not(:last-child) dl::before { border-width: 26px; border-top-color: var(--color-primary); }
|
|
124
|
+
.b-flow-boxed > li.is-focus:not(:last-child) dl::before { border-top-color: var(--color-accent); }
|
|
125
|
+
.b-flow-boxed > li:not(:last-child) dl::after { border-width: 23px; border-top-color: var(--color-surface); }
|
|
126
|
+
.b-flow-boxed > li dl dt { flex: 0 0 24%; margin-right: 2vw; text-align: center; }
|
|
127
|
+
.b-flow-boxed > li dl dt .icon { padding: 4px 14px; margin: 0 auto 6px; display: block; border-radius: 999px; width: fit-content; position: relative; z-index: 2; }
|
|
128
|
+
.b-flow-boxed > li dl dd { flex: 1; }
|
|
129
|
+
.b-flow-boxed > li dl dd .phase { display: block; width: fit-content; margin: 0 0 4px; }
|
|
130
|
+
|
|
131
|
+
/* スタイリッシュ: 全体を 1 つの枠で囲み、区切り線に ▽ */
|
|
132
|
+
.b-flow-framed { border: 4px solid var(--color-primary); }
|
|
133
|
+
.b-flow-framed > li { padding: 12px 36px; position: relative; }
|
|
134
|
+
.b-flow-framed > li:not(:last-child) { border-bottom: 4px solid var(--color-primary); }
|
|
135
|
+
.b-flow-framed > li:not(:last-child)::before, .b-flow-framed > li:not(:last-child)::after { content: ""; border: solid transparent; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); z-index: 1; }
|
|
136
|
+
.b-flow-framed > li:not(:last-child)::before { border-width: 26px; border-top-color: var(--color-primary); }
|
|
137
|
+
.b-flow-framed > li:not(:last-child)::after { border-width: 21px; border-top-color: var(--color-surface); }
|
|
138
|
+
.b-flow-framed > li .via { position: absolute; top: 0; left: calc(50% + 44px); transform: translateY(-50%); background: var(--color-surface); padding: 0 10px; z-index: 2; }
|
|
139
|
+
.b-flow-framed > li dl { display: flex; align-items: center; }
|
|
140
|
+
.b-flow-framed > li dl dt { flex: 0 0 24%; margin-right: 1vw; text-align: center; }
|
|
141
|
+
.b-flow-framed > li dl dt .icon { padding: 4px 14px; margin: 0 auto 6px; display: block; border-radius: 999px; width: fit-content; }
|
|
142
|
+
.b-flow-framed > li dl dd { flex: 1; }
|
|
143
|
+
.b-flow-framed > li dl dd .phase { display: block; width: fit-content; margin: 0 0 4px; }
|
|
144
|
+
|
|
145
|
+
/* 2 カラム構成: 右端が矢印の枠を 2 列に */
|
|
146
|
+
.b-flow-grid { display: flex; flex-wrap: wrap; gap: 48px 4%; }
|
|
147
|
+
.b-flow-grid > li { padding: 28px 44px 24px 36px; flex: 0 0 48%; border: 3px solid var(--color-primary); position: relative; background: var(--color-surface); }
|
|
148
|
+
.b-flow-grid > li.is-focus { border-color: var(--color-accent); }
|
|
149
|
+
.b-flow-grid > li:not(:last-child)::before, .b-flow-grid > li:not(:last-child)::after { content: ""; border: solid transparent; position: absolute; top: 50%; left: 100%; transform: translateY(-50%); }
|
|
150
|
+
.b-flow-grid > li:not(:last-child)::before { border-width: 30px; border-left-color: var(--color-primary); }
|
|
151
|
+
.b-flow-grid > li.is-focus:not(:last-child)::before { border-left-color: var(--color-accent); }
|
|
152
|
+
.b-flow-grid > li:not(:last-child)::after { border-width: 27px; border-left-color: var(--color-surface); }
|
|
153
|
+
.b-flow-grid > li .icon { padding: 7px 28px; display: block; border-radius: 999px; position: absolute; top: -18px; left: -14px; z-index: 2; }
|
|
154
|
+
.b-flow-grid > li .via { display: none; }
|
|
155
|
+
.b-flow-grid > li dl dt { margin-bottom: 6px; }
|
|
156
|
+
}
|
|
157
|
+
`;
|