@cardenelabs/cdl 0.5.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 +343 -0
- package/SPEC.md +374 -0
- package/dist/index.cjs +28085 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3415 -0
- package/dist/index.d.ts +3415 -0
- package/dist/index.js +27962 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +23043 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +2 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +23041 -0
- package/dist/react.js.map +1 -0
- package/dist/render-C78lIXeC.d.cts +2449 -0
- package/dist/render-C78lIXeC.d.ts +2449 -0
- package/examples/quick-start.md +88 -0
- package/package.json +79 -0
- package/src/anim/core/easing.ts +77 -0
- package/src/anim/core/timeline.ts +335 -0
- package/src/anim/core/types.ts +42 -0
- package/src/anim/index.ts +20 -0
- package/src/anim/react/index.ts +3 -0
- package/src/anim/react/useReducedMotion.ts +27 -0
- package/src/anim/react/useTimeline.ts +48 -0
- package/src/assert-never.ts +16 -0
- package/src/author-intent-verify.ts +587 -0
- package/src/builder.ts +3740 -0
- package/src/compile.ts +12 -0
- package/src/dom-verify-core.ts +121 -0
- package/src/dom-verify-types.ts +21 -0
- package/src/dom-verify.ts +948 -0
- package/src/event-handler/index.ts +184 -0
- package/src/formula/ast.ts +46 -0
- package/src/formula/evaluator.ts +163 -0
- package/src/formula/formula-computeds.ts +83 -0
- package/src/formula/index.ts +5 -0
- package/src/formula/parser.ts +399 -0
- package/src/index.ts +284 -0
- package/src/input-signals.ts +74 -0
- package/src/kinds/actor.tsx +79 -0
- package/src/kinds/card.tsx +61 -0
- package/src/kinds/chart-bar.tsx +121 -0
- package/src/kinds/chart-line.tsx +163 -0
- package/src/kinds/chart-pie.tsx +107 -0
- package/src/kinds/compact-title.ts +164 -0
- package/src/kinds/dyn-shape.tsx +394 -0
- package/src/kinds/event.tsx +70 -0
- package/src/kinds/function.tsx +53 -0
- package/src/kinds/funnel.tsx +122 -0
- package/src/kinds/gantt.tsx +193 -0
- package/src/kinds/generic.tsx +368 -0
- package/src/kinds/mind-map.tsx +367 -0
- package/src/kinds/mind-radial.tsx +209 -0
- package/src/kinds/node-tone.ts +18 -0
- package/src/kinds/quadrant.tsx +164 -0
- package/src/kinds/row-align.ts +179 -0
- package/src/kinds/shape-basement.tsx +683 -0
- package/src/kinds/shape-blockchain.tsx +750 -0
- package/src/kinds/shape-commerce.tsx +553 -0
- package/src/kinds/shape-finance.tsx +706 -0
- package/src/kinds/shape-hardware.tsx +862 -0
- package/src/kinds/shape-people.tsx +439 -0
- package/src/kinds/shape-region.tsx +383 -0
- package/src/kinds/shape-software.tsx +859 -0
- package/src/kinds/storage.tsx +180 -0
- package/src/kinds/text-width.ts +73 -0
- package/src/kinds/tree.tsx +275 -0
- package/src/kinds/user-journey.tsx +240 -0
- package/src/label-text.ts +71 -0
- package/src/layout/clearance-constants.ts +47 -0
- package/src/layout/collisions.ts +2078 -0
- package/src/layout/edges.ts +2498 -0
- package/src/layout/footer-shape.ts +97 -0
- package/src/layout/geometry.ts +135 -0
- package/src/layout/label-shift.ts +28 -0
- package/src/layout/lanes.ts +328 -0
- package/src/layout/nodes.ts +190 -0
- package/src/layout/predict-bbox.ts +76 -0
- package/src/layout/px-projection.ts +176 -0
- package/src/layout/spec.ts +872 -0
- package/src/layout/text-width.ts +133 -0
- package/src/layout/tokens.ts +181 -0
- package/src/layout/viewbox.ts +47 -0
- package/src/layout-with-validation.ts +175 -0
- package/src/layout.ts +381 -0
- package/src/presets.ts +2108 -0
- package/src/reactive/batch.ts +48 -0
- package/src/reactive/computed.ts +85 -0
- package/src/reactive/effect.ts +145 -0
- package/src/reactive/index.ts +6 -0
- package/src/reactive/internal.ts +109 -0
- package/src/reactive/signal.ts +113 -0
- package/src/render/edges.tsx +318 -0
- package/src/render/header.tsx +146 -0
- package/src/render/interactive-panel.tsx +9620 -0
- package/src/render/nodes.tsx +319 -0
- package/src/render/stage.tsx +377 -0
- package/src/render/tone.ts +64 -0
- package/src/render/utils.ts +238 -0
- package/src/render.tsx +221 -0
- package/src/scroll-trigger/index.ts +109 -0
- package/src/scroll-trigger/progress.ts +49 -0
- package/src/thumbnail.tsx +114 -0
- package/src/types.ts +2371 -0
- package/src/validate.ts +268 -0
- package/src/visual-validate.ts +4381 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import { shapeRegion, labelPlan, fitArt } from "./shape-region";
|
|
3
|
+
import type { LaidNode } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shape-driven commerce / web 6 kinds (CAR-1111 Phase 2-D)。
|
|
7
|
+
*
|
|
8
|
+
* website / storefront / warehouse / online-shop / cdn-edge / api-gateway。
|
|
9
|
+
*
|
|
10
|
+
* 設計原則。
|
|
11
|
+
* 1. shape 本体は node.cx, node.cy を中心に上下左右対称に配置
|
|
12
|
+
* 2. 中央に 1 つの視覚シンボルを大きく置く、 細部は削って silhouette 優先
|
|
13
|
+
* 3. label は shape の下に固定
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
type ShapeProps = {
|
|
17
|
+
node: LaidNode;
|
|
18
|
+
active: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const commonFrame = (active: boolean) => ({
|
|
22
|
+
fill: "var(--cdl-node-fill, #ffffff)",
|
|
23
|
+
stroke: active ? "var(--cdl-tone-accent, #2d6a8f)" : "var(--cdl-divider, #d4d4d8)",
|
|
24
|
+
strokeWidth: active ? 3 : 1.5,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/** 肩書の字間。 幅で縮める時はこれも同じ割合で詰める。 */
|
|
28
|
+
const LABEL_EYEBROW_SPACING = 2;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 1 字の幅は字の大きさの何倍か。 呼び出し側の書体
|
|
32
|
+
* (`"Inter Variable", Inter, -apple-system, "Segoe UI", sans-serif`) を太字 700 で実測した値。
|
|
33
|
+
*
|
|
34
|
+
* **一律の係数は使えない**。 半角の実測は最小 0.271 (`i` / `j` / `l`) から最大 1.038 (`W`) まで
|
|
35
|
+
* 4 倍近く開く。 平均の 0.630 で置くと `W` の並びで 3 分の 2 しか見ず、 その比で縮めても箱から
|
|
36
|
+
* 出たままになる (`W` 20 字が実測 390.4 に対し 0.7 の見積りで 224、 縮めた後も 86 出た)。
|
|
37
|
+
*
|
|
38
|
+
* | 階級 | 実測の最大 | 係数 |
|
|
39
|
+
* |---|---|---|
|
|
40
|
+
* | 細い (`i j l I . , : ; '` `!` と空白) | 0.365 | 0.37 |
|
|
41
|
+
* | 広い (`W @ % M m`) | 1.038 (`W`) | 1.04 |
|
|
42
|
+
* | その他の ASCII | 0.850 (`w`) | 0.86 |
|
|
43
|
+
* | ASCII の外 | 1.000 (和文) | 1.04 |
|
|
44
|
+
*
|
|
45
|
+
* 並 400 の最大は 0.985 なので太字の値で覆える。
|
|
46
|
+
*/
|
|
47
|
+
const 細字 = new Set([..."ijlI.,:;'! "]);
|
|
48
|
+
const 広字 = new Set([..."W@%Mm"]);
|
|
49
|
+
const 細字比 = 0.37;
|
|
50
|
+
const 広字比 = 1.04;
|
|
51
|
+
const 並字比 = 0.86;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 1 字が字の大きさの何倍か。
|
|
55
|
+
*
|
|
56
|
+
* **ASCII の外は分類しない**。 「どの字が全角か」 を手で列挙する限り抜けは出続け、 抜けは必ず
|
|
57
|
+
* **過小見積り** = 収まっていない物を収まっていると判定する向きに外れる。 実際に 3 回とも別の
|
|
58
|
+
* 抜け方をした (範囲の端点に見た目の同じ字が入る / 私用領域が混じる / 区画指定からカタカナ拡張と
|
|
59
|
+
* CJK 互換が落ちる)。
|
|
60
|
+
*
|
|
61
|
+
* ASCII の外は実測の最大 `1.04` で数える。 全角の実測は 1.000 なので 4% 広く見積もるだけで、
|
|
62
|
+
* 半角カナ (約 0.5) も外字も Latin-1 もギリシャ文字もまとめて安全側に倒れる。 代償は和文の名前が
|
|
63
|
+
* 4% 余分に縮むことだけで、 過小見積りが構造的に起きなくなる。
|
|
64
|
+
*/
|
|
65
|
+
function 字の比(c: string): number {
|
|
66
|
+
if (細字.has(c)) return 細字比;
|
|
67
|
+
if (広字.has(c)) return 広字比;
|
|
68
|
+
return (c.codePointAt(0) ?? 0) < 128 ? 並字比 : 広字比;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 字の並びの幅の見積り。 字ごとの比を足して、 字間を字数分足す。 */
|
|
72
|
+
function 字幅(文字列: string, 大きさ: number, 字間: number): number {
|
|
73
|
+
let 幅 = 0;
|
|
74
|
+
let 字数 = 0;
|
|
75
|
+
for (const c of 文字列) {
|
|
76
|
+
幅 += 大きさ * 字の比(c);
|
|
77
|
+
字数 += 1;
|
|
78
|
+
}
|
|
79
|
+
return 幅 + 字間 * 字数;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 1 行を箱の幅に収める字の大きさと字間。
|
|
84
|
+
*
|
|
85
|
+
* `labelPlan` は箱の **高さ** だけから字の大きさを決めていて、 幅も字数も見ていない。 その
|
|
86
|
+
* ままだと長い名前が箱の左右へ出る (幅 140 / 高さ 72 で `LongApplicationService` は 12px で
|
|
87
|
+
* 175.8 = 左右へ 17.9)。 絵と違って `fitArt` は名前を包まないので、 ここで行ごとに縮める。
|
|
88
|
+
*
|
|
89
|
+
* 字間も同じ割合で詰める = 大きさだけ縮めても字間が残ると幅が比例して縮まない。
|
|
90
|
+
*/
|
|
91
|
+
function 幅に収める(文字列: string, 大きさ: number, 字間: number, 箱幅: number) {
|
|
92
|
+
const 幅 = 字幅(文字列, 大きさ, 字間);
|
|
93
|
+
const 倍率 = 幅 > 0 && 箱幅 > 0 ? Math.min(1, 箱幅 / 幅) : 1;
|
|
94
|
+
return { size: 大きさ * 倍率, spacing: 字間 * 倍率 };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 名前の塊を描く。 縦の位置と大きさは `labelPlan` が決め (箱の下端から積み上げる、 #433)、
|
|
99
|
+
* 横は `幅に収める` が箱の幅に合わせて縮める。
|
|
100
|
+
*/
|
|
101
|
+
function bottomLabel(node: LaidNode, cx: number, plan: ReturnType<typeof labelPlan>) {
|
|
102
|
+
const 肩 = 幅に収める(node.eyebrow ?? "", plan.肩書?.size ?? 0, LABEL_EYEBROW_SPACING, node.w);
|
|
103
|
+
const 名 = 幅に収める(node.title ?? "", plan.名前.size, 0, node.w);
|
|
104
|
+
const 説 = 幅に収める(node.subtitle ?? "", plan.説明?.size ?? 0, 0, node.w);
|
|
105
|
+
return (
|
|
106
|
+
<>
|
|
107
|
+
{node.eyebrow && plan.肩書 && (
|
|
108
|
+
<text x={cx} y={plan.肩書.y} fontSize={肩.size} fontWeight={700} letterSpacing={肩.spacing} textAnchor="middle" fill="var(--cdl-tone-accent, #2d6a8f)">
|
|
109
|
+
{node.eyebrow}
|
|
110
|
+
</text>
|
|
111
|
+
)}
|
|
112
|
+
<text data-cdl-role="node-label" x={cx} y={plan.名前.y} fontSize={名.size} fontWeight={700} textAnchor="middle" fill="var(--cdl-text, #1a1f2a)">
|
|
113
|
+
{node.title}
|
|
114
|
+
</text>
|
|
115
|
+
{node.subtitle && plan.説明 && (
|
|
116
|
+
<text x={cx} y={plan.説明.y} fontSize={説.size} textAnchor="middle" fill="var(--cdl-text-dim, #5a6270)">
|
|
117
|
+
{node.subtitle}
|
|
118
|
+
</text>
|
|
119
|
+
)}
|
|
120
|
+
</>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 落とした影 (`shape-website` / `shape-api-gateway`) が本体の外へ出る量 (#433)。
|
|
127
|
+
*
|
|
128
|
+
* 影は本体を右下へ 4 ずらした複製なので、 右と下へ 4 出る。 枠も中心線から半分外へ出る。
|
|
129
|
+
* どちらも **箱の高さに依らない** 固定量なので、 本体の寸法を決める前に描ける帯をこの分だけ
|
|
130
|
+
* 先に狭める。
|
|
131
|
+
*/
|
|
132
|
+
const DROP_SHADOW_D = 4;
|
|
133
|
+
|
|
134
|
+
/** ブラウザ (`shape-website`) の中身が潰れない大きさ。 */
|
|
135
|
+
const WEBSITE_BAR_H = 32;
|
|
136
|
+
/** 見出し帯 40 + 上下の間 48 + 板 10。 */
|
|
137
|
+
const WEBSITE_H_MIN = WEBSITE_BAR_H + 98;
|
|
138
|
+
const WEBSITE_W_MIN = 160;
|
|
139
|
+
const WEBSITE_W_MAX = 280;
|
|
140
|
+
|
|
141
|
+
/** shape-website = ブラウザ (シンプル + シンプルレイアウト) */
|
|
142
|
+
export function ShapeWebsiteNode({ node, active }: ShapeProps): JSX.Element {
|
|
143
|
+
const frame = commonFrame(active);
|
|
144
|
+
const r = shapeRegion(node);
|
|
145
|
+
const cx = r.cx;
|
|
146
|
+
const 余白 = Math.max(DROP_SHADOW_D, frame.strokeWidth / 2);
|
|
147
|
+
// 帯を狭めてから本体の大きさを決める。 中身が潰れる箱では下限で止め、 入り切らない分は
|
|
148
|
+
// `fitArt` が絵ごと縮める (縦横の比は保たれる)
|
|
149
|
+
const 縦 = Math.max(r.availH, WEBSITE_H_MIN + 余白 * 2);
|
|
150
|
+
const bh = 縦 - 余白 * 2;
|
|
151
|
+
const bw = Math.min(Math.max(r.availW - 余白 * 2, WEBSITE_W_MIN), WEBSITE_W_MAX);
|
|
152
|
+
const x = cx - bw / 2;
|
|
153
|
+
const y = r.cy - 縦 / 2 + 余白;
|
|
154
|
+
const 広がり = { 上: 縦 / 2, 下: 縦 / 2, 左: bw / 2 + 余白, 右: bw / 2 + 余白 };
|
|
155
|
+
const barH = WEBSITE_BAR_H;
|
|
156
|
+
const 絵 = (
|
|
157
|
+
<g>
|
|
158
|
+
<rect x={x + DROP_SHADOW_D} y={y + DROP_SHADOW_D} width={bw} height={bh} rx={10} fill={frame.stroke} opacity={0.2} stroke="none" />
|
|
159
|
+
<rect data-cdl-role="node-body" x={x} y={y} width={bw} height={bh} rx={10} {...frame} />
|
|
160
|
+
<path d={`M ${x} ${y + barH} L ${x} ${y + 10} Q ${x} ${y}, ${x + 10} ${y} L ${x + bw - 10} ${y} Q ${x + bw} ${y}, ${x + bw} ${y + 10} L ${x + bw} ${y + barH} Z`} fill="var(--cdl-divider, #d4d4d8)" fillOpacity={0.5} stroke="none" />
|
|
161
|
+
<line x1={x} y1={y + barH} x2={x + bw} y2={y + barH} stroke={frame.stroke} strokeWidth={1} />
|
|
162
|
+
<circle cx={x + 14} cy={y + barH / 2} r={5} fill="#ff5f56" stroke="none" />
|
|
163
|
+
<circle cx={x + 28} cy={y + barH / 2} r={5} fill="#ffbd2e" stroke="none" />
|
|
164
|
+
<circle cx={x + 42} cy={y + barH / 2} r={5} fill="#27c93f" stroke="none" />
|
|
165
|
+
<rect x={x + 60} y={y + 8} width={bw - 76} height={16} rx={8} fill="var(--cdl-node-fill, #ffffff)" stroke={frame.stroke} strokeWidth={1.2} />
|
|
166
|
+
{/* シンプル content = hero + 3 tiles */}
|
|
167
|
+
<rect x={x + 20} y={y + barH + 16} width={bw - 40} height={40} rx={4} fill="var(--cdl-tone-accent, #2d6a8f)" fillOpacity={0.6} stroke="none" />
|
|
168
|
+
{[0, 1, 2].map((i) => (
|
|
169
|
+
<rect key={i} x={x + 20 + i * ((bw - 40) / 3 + 6)} y={y + barH + 66} width={(bw - 40) / 3 - 4} height={bh - barH - 88} rx={4} fill="var(--cdl-divider, #d4d4d8)" fillOpacity={0.35} stroke={frame.stroke} strokeWidth={0.8} />
|
|
170
|
+
))}
|
|
171
|
+
</g>
|
|
172
|
+
);
|
|
173
|
+
return (
|
|
174
|
+
<g>
|
|
175
|
+
{fitArt(r, 広がり, 絵)}
|
|
176
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
177
|
+
</g>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** shape-storefront = S-A シンプル shop (平屋根 + SHOP 看板 + 大 window (商品) + 中央 door + OPEN sign) */
|
|
182
|
+
export function ShapeStorefrontNode({ node, active }: ShapeProps): JSX.Element {
|
|
183
|
+
const frame = commonFrame(active);
|
|
184
|
+
const r = shapeRegion(node);
|
|
185
|
+
const cx = r.cx;
|
|
186
|
+
const cy = r.cy + 4;
|
|
187
|
+
const bw = Math.min(r.availW - 40, 220);
|
|
188
|
+
const bh = Math.min(r.availH - 40, 220);
|
|
189
|
+
const x = cx - bw / 2;
|
|
190
|
+
const y = cy - bh / 2;
|
|
191
|
+
const signH = 26;
|
|
192
|
+
const accent = "var(--cdl-tone-accent, #2d6a8f)";
|
|
193
|
+
const accentDark = "#1a4a68";
|
|
194
|
+
const accentDarker = "#0f3450";
|
|
195
|
+
// window / door 領域
|
|
196
|
+
const facadeY = y + signH;
|
|
197
|
+
const facadeH = bh - signH - 12;
|
|
198
|
+
const winW = bw * 0.42;
|
|
199
|
+
const winH = facadeH - 32;
|
|
200
|
+
const winX = x + 12;
|
|
201
|
+
const winY = facadeY + 16;
|
|
202
|
+
const doorW = bw * 0.36;
|
|
203
|
+
const doorH = facadeH - 30;
|
|
204
|
+
const doorX = x + bw - doorW - 12;
|
|
205
|
+
const doorY = facadeY + 20;
|
|
206
|
+
return (
|
|
207
|
+
<g>
|
|
208
|
+
{/* drop shadow */}
|
|
209
|
+
<ellipse cx={cx} cy={y + bh + 8} rx={bw * 0.5} ry={5} fill={frame.stroke} opacity={0.2} stroke="none" />
|
|
210
|
+
{/* SHOP 看板 (accent) */}
|
|
211
|
+
<rect x={x} y={y} width={bw} height={signH} fill={accent} stroke={accentDark} strokeWidth={2} />
|
|
212
|
+
<text x={cx} y={y + 18} fontSize={16} fontWeight={900} letterSpacing={8} textAnchor="middle" fill="#ffffff">SHOP</text>
|
|
213
|
+
{/* facade (店舗本体 白ベース) */}
|
|
214
|
+
<rect data-cdl-role="node-body" x={x} y={facadeY} width={bw} height={facadeH} fill="#e0e4e8" stroke="#5a6270" strokeWidth={frame.strokeWidth} />
|
|
215
|
+
{/* 大 window (商品ディスプレイ、 左) */}
|
|
216
|
+
<rect x={winX} y={winY} width={winW} height={winH} fill="#a4c5d9" stroke={accentDarker} strokeWidth={2} />
|
|
217
|
+
<line x1={winX + winW / 2} y1={winY} x2={winX + winW / 2} y2={winY + winH} stroke={accentDarker} strokeWidth={1} />
|
|
218
|
+
<line x1={winX} y1={winY + winH / 2} x2={winX + winW} y2={winY + winH / 2} stroke={accentDarker} strokeWidth={1} />
|
|
219
|
+
{/* 商品 4 個 (window 内、 各象限) */}
|
|
220
|
+
<rect x={winX + 6} y={winY + 6} width={winW / 2 - 10} height={winH / 2 - 10} rx={2} fill="#c15a4a" />
|
|
221
|
+
<rect x={winX + winW / 2 + 4} y={winY + 6} width={winW / 2 - 10} height={winH / 2 - 10} rx={2} fill="#4ea36a" />
|
|
222
|
+
<rect x={winX + 6} y={winY + winH / 2 + 4} width={winW / 2 - 10} height={winH / 2 - 10} rx={2} fill="#e5c07b" />
|
|
223
|
+
<rect x={winX + winW / 2 + 4} y={winY + winH / 2 + 4} width={winW / 2 - 10} height={winH / 2 - 10} rx={2} fill="#a4b5f2" />
|
|
224
|
+
{/* door (右、 accentGold 木製) */}
|
|
225
|
+
<rect x={doorX} y={doorY} width={doorW} height={doorH} fill="#a89d8f" stroke="#5a6270" strokeWidth={2} />
|
|
226
|
+
<rect x={doorX + 4} y={doorY + 4} width={doorW - 8} height={doorH - 8} fill="#8b949e" stroke={accentDarker} strokeWidth={1.5} />
|
|
227
|
+
{/* door の水平 line 装飾 */}
|
|
228
|
+
<line x1={doorX + 4} y1={doorY + doorH * 0.38} x2={doorX + doorW - 4} y2={doorY + doorH * 0.38} stroke={accentDarker} strokeWidth={1} />
|
|
229
|
+
<line x1={doorX + 4} y1={doorY + doorH * 0.72} x2={doorX + doorW - 4} y2={doorY + doorH * 0.72} stroke={accentDarker} strokeWidth={1} />
|
|
230
|
+
{/* OPEN sign (door 下部に貼付) */}
|
|
231
|
+
<rect x={doorX + doorW / 2 - 20} y={doorY + doorH * 0.85} width={40} height={12} fill="#e5c07b" stroke="#a67c00" strokeWidth={1.5} />
|
|
232
|
+
<text x={doorX + doorW / 2} y={doorY + doorH * 0.85 + 9} fontSize={8} fontWeight={700} letterSpacing={1} textAnchor="middle" fill="#5a3a10">OPEN</text>
|
|
233
|
+
{/* door handle (小さな dot) */}
|
|
234
|
+
<circle cx={doorX + 6} cy={doorY + doorH * 0.55} r={1.5} fill="#c15a4a" />
|
|
235
|
+
{/* base 床 (前 storefront 直下のみ) */}
|
|
236
|
+
<rect x={x - 6} y={y + bh - 6} width={bw + 12} height={8} fill="#5a6270" stroke="none" />
|
|
237
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
238
|
+
</g>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 倉庫 (`shape-warehouse`) の固定部品 (#433)。
|
|
244
|
+
*
|
|
245
|
+
* 屋根と床は建物の左右へ、 影は下へ出る。 出る量は箱の高さに依らないので、 建物の寸法を
|
|
246
|
+
* 決める前に描ける帯をこの分だけ先に狭める。
|
|
247
|
+
*/
|
|
248
|
+
const WAREHOUSE_ROOF_H = 18;
|
|
249
|
+
const WAREHOUSE_SIGN_H = 24;
|
|
250
|
+
/** 屋根が建物の左右へ出る量と、 その枠の太さ。 */
|
|
251
|
+
const WAREHOUSE_ROOF_OUT = 4;
|
|
252
|
+
const WAREHOUSE_ROOF_SW = 2;
|
|
253
|
+
/** 床が建物の左右へ出る量。 */
|
|
254
|
+
const WAREHOUSE_FLOOR_OUT = 6;
|
|
255
|
+
/** 影の楕円を建物の下端からどれだけ下に置くか、 その縦半径と横半径の比。 */
|
|
256
|
+
const WAREHOUSE_SHADOW_DY = 8;
|
|
257
|
+
const WAREHOUSE_SHADOW_RY = 5;
|
|
258
|
+
const WAREHOUSE_SHADOW_RX_RATIO = 0.5;
|
|
259
|
+
/** 建物の大きさ。 高さの下限は shutter が潰れない高さ。 */
|
|
260
|
+
const WAREHOUSE_H_MIN = WAREHOUSE_ROOF_H + WAREHOUSE_SIGN_H + 68;
|
|
261
|
+
const WAREHOUSE_W_MIN = 160;
|
|
262
|
+
const WAREHOUSE_W_MAX = 240;
|
|
263
|
+
|
|
264
|
+
/** shape-warehouse = W-A 大 shutter (平屋根 + WAREHOUSE 看板 + 中央大 shutter 10 rib + loading dock) */
|
|
265
|
+
export function ShapeWarehouseNode({ node, active }: ShapeProps): JSX.Element {
|
|
266
|
+
const frame = commonFrame(active);
|
|
267
|
+
const r = shapeRegion(node);
|
|
268
|
+
const cx = r.cx;
|
|
269
|
+
// 屋根の頂点は建物の上端より 2 下なので、 上には何も出ない
|
|
270
|
+
const 上出 = 0;
|
|
271
|
+
const 下出 = WAREHOUSE_SHADOW_DY + WAREHOUSE_SHADOW_RY;
|
|
272
|
+
const 横出 = Math.max(
|
|
273
|
+
WAREHOUSE_FLOOR_OUT,
|
|
274
|
+
WAREHOUSE_ROOF_OUT + WAREHOUSE_ROOF_SW / 2,
|
|
275
|
+
frame.strokeWidth / 2,
|
|
276
|
+
);
|
|
277
|
+
const 縦 = Math.max(r.availH, WAREHOUSE_H_MIN + 上出 + 下出);
|
|
278
|
+
const bh = 縦 - 上出 - 下出;
|
|
279
|
+
const bw = Math.min(Math.max(r.availW - 横出 * 2, WAREHOUSE_W_MIN), WAREHOUSE_W_MAX);
|
|
280
|
+
const x = cx - bw / 2;
|
|
281
|
+
const y = r.cy - 縦 / 2 + 上出;
|
|
282
|
+
// 影の横半径は建物の半幅ちょうどなので、 左右を決めるのは常に床の出幅の側になる
|
|
283
|
+
const 広がり = { 上: 縦 / 2, 下: 縦 / 2, 左: bw / 2 + 横出, 右: bw / 2 + 横出 };
|
|
284
|
+
const roofH = WAREHOUSE_ROOF_H;
|
|
285
|
+
const signH = WAREHOUSE_SIGN_H;
|
|
286
|
+
const shutterY = y + roofH + signH + 8;
|
|
287
|
+
const shutterX = x + 20;
|
|
288
|
+
const shutterW = bw - 40;
|
|
289
|
+
const shutterH = bh - roofH - signH - 32;
|
|
290
|
+
const accentDarker = "#0f3450";
|
|
291
|
+
const 絵 = (
|
|
292
|
+
<g>
|
|
293
|
+
{/* drop shadow */}
|
|
294
|
+
<ellipse cx={cx} cy={y + bh + WAREHOUSE_SHADOW_DY} rx={bw * WAREHOUSE_SHADOW_RX_RATIO} ry={WAREHOUSE_SHADOW_RY} fill={frame.stroke} opacity={0.2} stroke="none" />
|
|
295
|
+
{/* 平屋根 (斜め) */}
|
|
296
|
+
<path d={`M ${x - WAREHOUSE_ROOF_OUT} ${y + roofH} L ${x + 12} ${y + 2} L ${x + bw - 12} ${y + 2} L ${x + bw + WAREHOUSE_ROOF_OUT} ${y + roofH} Z`} fill="#5a6270" stroke={accentDarker} strokeWidth={WAREHOUSE_ROOF_SW} strokeLinejoin="round" />
|
|
297
|
+
{/* 倉庫 body (灰色壁) */}
|
|
298
|
+
<rect data-cdl-role="node-body" x={x} y={y + roofH} width={bw} height={bh - roofH - 12} fill="#a89d8f" stroke="#5a6270" strokeWidth={frame.strokeWidth} />
|
|
299
|
+
{/* WAREHOUSE 看板 (赤帯) */}
|
|
300
|
+
<rect x={x} y={y + roofH} width={bw} height={signH} fill="#c15a4a" stroke="#8b3f2f" strokeWidth={1.5} />
|
|
301
|
+
<text x={cx} y={y + roofH + 17} fontSize={12} fontWeight={900} letterSpacing={6} textAnchor="middle" fill="#ffffff">WAREHOUSE</text>
|
|
302
|
+
{/* 中央大 shutter (rib 縞) */}
|
|
303
|
+
<rect x={shutterX} y={shutterY} width={shutterW} height={shutterH} fill="#8b949e" stroke={accentDarker} strokeWidth={2} />
|
|
304
|
+
{/* shutter 上部 shade (立体感) */}
|
|
305
|
+
<rect x={shutterX} y={shutterY} width={shutterW} height={5} fill="#5a6270" stroke="none" />
|
|
306
|
+
{/* shutter 10 rib horizontal lines */}
|
|
307
|
+
{Array.from({ length: 10 }, (_, i) => i).map((i) => (
|
|
308
|
+
<line
|
|
309
|
+
key={`rib-${i}`}
|
|
310
|
+
x1={shutterX}
|
|
311
|
+
y1={shutterY + 6 + (i + 1) * ((shutterH - 6) / 11)}
|
|
312
|
+
x2={shutterX + shutterW}
|
|
313
|
+
y2={shutterY + 6 + (i + 1) * ((shutterH - 6) / 11)}
|
|
314
|
+
stroke="#5a6270"
|
|
315
|
+
strokeWidth={1}
|
|
316
|
+
/>
|
|
317
|
+
))}
|
|
318
|
+
{/* loading dock (底部黒帯) */}
|
|
319
|
+
<rect x={shutterX} y={y + bh - 22} width={shutterW} height={10} fill={accentDarker} stroke="#000" strokeWidth={1} />
|
|
320
|
+
{/* dock 中央 loading warning stripe (yellow) */}
|
|
321
|
+
<rect x={cx - 10} y={y + bh - 20} width={20} height={16} rx={2} fill="#e5c07b" stroke="#a67c00" strokeWidth={1} />
|
|
322
|
+
<circle cx={cx} cy={y + bh - 14} r={4} fill="#5a3a10" />
|
|
323
|
+
{/* base 床 (直下のみ) */}
|
|
324
|
+
<rect x={x - WAREHOUSE_FLOOR_OUT} y={y + bh - 12} width={bw + WAREHOUSE_FLOOR_OUT * 2} height={6} fill="#5a6270" stroke="none" />
|
|
325
|
+
<rect x={x - WAREHOUSE_FLOOR_OUT} y={y + bh - 6} width={bw + WAREHOUSE_FLOOR_OUT * 2} height={4} fill={accentDarker} stroke="none" />
|
|
326
|
+
</g>
|
|
327
|
+
);
|
|
328
|
+
return (
|
|
329
|
+
<g>
|
|
330
|
+
{fitArt(r, 広がり, 絵)}
|
|
331
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
332
|
+
</g>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** shape-online-shop = O-A 大 shopping cart (縦長 260x320、 中身なし + badge なし = カート単体 + 車輪 + ハンドル + 籠 grid) */
|
|
337
|
+
export function ShapeOnlineShopNode({ node, active }: ShapeProps): JSX.Element {
|
|
338
|
+
const frame = commonFrame(active);
|
|
339
|
+
const r = shapeRegion(node);
|
|
340
|
+
const cx = r.cx;
|
|
341
|
+
const cy = r.cy + 10;
|
|
342
|
+
const scale = Math.min(r.availW / 260, r.availH / 320);
|
|
343
|
+
const s = scale * 0.9;
|
|
344
|
+
const accent = "var(--cdl-tone-accent, #2d6a8f)";
|
|
345
|
+
const accentDark = "#1a4a68";
|
|
346
|
+
const accentDarker = "#0f3450";
|
|
347
|
+
// カート本体座標 (中央基準、 s = scale factor)
|
|
348
|
+
const basketTopY = cy - 40 * s;
|
|
349
|
+
const basketBotY = cy + 50 * s;
|
|
350
|
+
const basketTopL = cx - 68 * s;
|
|
351
|
+
const basketTopR = cx + 68 * s;
|
|
352
|
+
const basketBotL = cx - 54 * s;
|
|
353
|
+
const basketBotR = cx + 54 * s;
|
|
354
|
+
const wheelY = cy + 88 * s;
|
|
355
|
+
const wheelL = cx - 42 * s;
|
|
356
|
+
const wheelR = cx + 42 * s;
|
|
357
|
+
return (
|
|
358
|
+
<g>
|
|
359
|
+
{/* drop shadow */}
|
|
360
|
+
<ellipse cx={cx} cy={wheelY + 22 * s} rx={80 * s} ry={5 * s} fill={frame.stroke} opacity={0.25} stroke="none" />
|
|
361
|
+
{/* ハンドル (左上から下降) */}
|
|
362
|
+
<path d={`M ${cx - 118 * s} ${basketTopY - 60 * s} L ${cx - 90 * s} ${basketTopY - 60 * s} L ${cx - 80 * s} ${basketTopY - 30 * s} L ${basketTopL} ${basketTopY}`} fill="none" stroke={accent} strokeWidth={6 * s} strokeLinecap="round" strokeLinejoin="round" />
|
|
363
|
+
{/* ハンドル grip 端 (円) */}
|
|
364
|
+
<circle cx={cx - 118 * s} cy={basketTopY - 60 * s} r={4 * s} fill={accent} stroke={accentDark} strokeWidth={1.5} />
|
|
365
|
+
{/* 籠本体 (台形) */}
|
|
366
|
+
<path
|
|
367
|
+
data-cdl-role="node-body"
|
|
368
|
+
d={`M ${basketTopL} ${basketTopY} L ${basketTopR} ${basketTopY} L ${basketBotR} ${basketBotY} L ${basketBotL} ${basketBotY} Z`}
|
|
369
|
+
fill="#a4c5d9"
|
|
370
|
+
stroke={accentDarker}
|
|
371
|
+
strokeWidth={3 * s}
|
|
372
|
+
strokeLinejoin="round"
|
|
373
|
+
/>
|
|
374
|
+
{/* 籠 上端 rim (立体感) */}
|
|
375
|
+
<rect x={basketTopL} y={basketTopY - 6 * s} width={basketTopR - basketTopL} height={8 * s} fill={accent} stroke={accentDarker} strokeWidth={2 * s} />
|
|
376
|
+
{/* 籠 grid 縦線 (5 本) */}
|
|
377
|
+
{[0.17, 0.34, 0.5, 0.66, 0.83].map((rr, i) => {
|
|
378
|
+
const topX = basketTopL + (basketTopR - basketTopL) * rr;
|
|
379
|
+
const botX = basketBotL + (basketBotR - basketBotL) * rr;
|
|
380
|
+
return <line key={`v${i}`} x1={topX} y1={basketTopY} x2={botX} y2={basketBotY} stroke={accentDarker} strokeWidth={1.5 * s} />;
|
|
381
|
+
})}
|
|
382
|
+
{/* 籠 grid 横線 (3 本) */}
|
|
383
|
+
{[0.25, 0.5, 0.75].map((rr, i) => {
|
|
384
|
+
const leftX = basketTopL + (basketBotL - basketTopL) * rr;
|
|
385
|
+
const rightX = basketTopR + (basketBotR - basketTopR) * rr;
|
|
386
|
+
const yy = basketTopY + (basketBotY - basketTopY) * rr;
|
|
387
|
+
return <line key={`h${i}`} x1={leftX} y1={yy} x2={rightX} y2={yy} stroke={accentDarker} strokeWidth={1.5 * s} />;
|
|
388
|
+
})}
|
|
389
|
+
{/* 籠 下部 axis line (車輪への連結) */}
|
|
390
|
+
<line x1={basketBotL + 6 * s} y1={basketBotY} x2={wheelL} y2={wheelY - 12 * s} stroke={accent} strokeWidth={4 * s} strokeLinecap="round" />
|
|
391
|
+
<line x1={basketBotR - 6 * s} y1={basketBotY} x2={wheelR} y2={wheelY - 12 * s} stroke={accent} strokeWidth={4 * s} strokeLinecap="round" />
|
|
392
|
+
{/* 左車輪 */}
|
|
393
|
+
<circle cx={wheelL} cy={wheelY} r={16 * s} fill="#0f1830" stroke="#000" strokeWidth={2 * s} />
|
|
394
|
+
<circle cx={wheelL} cy={wheelY} r={8 * s} fill="#5a6270" stroke="#0f1830" strokeWidth={1.5 * s} />
|
|
395
|
+
<circle cx={wheelL} cy={wheelY} r={2 * s} fill="#0f1830" />
|
|
396
|
+
{/* 右車輪 */}
|
|
397
|
+
<circle cx={wheelR} cy={wheelY} r={16 * s} fill="#0f1830" stroke="#000" strokeWidth={2 * s} />
|
|
398
|
+
<circle cx={wheelR} cy={wheelY} r={8 * s} fill="#5a6270" stroke="#0f1830" strokeWidth={1.5 * s} />
|
|
399
|
+
<circle cx={wheelR} cy={wheelY} r={2 * s} fill="#0f1830" />
|
|
400
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
401
|
+
</g>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** shape-cdn-edge = 地球儀 (globe + 5 edge dots + pulse ring + lightning at center) */
|
|
406
|
+
export function ShapeCdnEdgeNode({ node, active }: ShapeProps): JSX.Element {
|
|
407
|
+
const frame = commonFrame(active);
|
|
408
|
+
const r = shapeRegion(node);
|
|
409
|
+
const cx = r.cx;
|
|
410
|
+
const cy = r.cy;
|
|
411
|
+
const rr = Math.min(r.availW, r.availH) * 0.32;
|
|
412
|
+
/**
|
|
413
|
+
* 絵の広がり。 **飾りの輪が本体よりずっと外まで届く**。
|
|
414
|
+
*
|
|
415
|
+
* 波紋は `rr + 12 + i * 16` から始まり、 動きで `+16` まで伸びる (`i = 1` の最大が
|
|
416
|
+
* `rr + 44`)。 節点は `rr * 1.1` の位置に半径 7 の丸を置き、 線が 2 の太さを持つ。
|
|
417
|
+
* 大きい方を採る。
|
|
418
|
+
*/
|
|
419
|
+
const 波紋 = rr + 44 + 1.5 / 2;
|
|
420
|
+
const 節点 = rr * 1.1 + 7 + 2 / 2;
|
|
421
|
+
const 広がり = Math.max(波紋, 節点);
|
|
422
|
+
const 絵 = (
|
|
423
|
+
<g>
|
|
424
|
+
{[0, 1].map((i) => (
|
|
425
|
+
<circle
|
|
426
|
+
key={i}
|
|
427
|
+
cx={cx}
|
|
428
|
+
cy={cy}
|
|
429
|
+
r={rr + 12 + i * 16}
|
|
430
|
+
fill="none"
|
|
431
|
+
stroke="var(--cdl-tone-accent, #2d6a8f)"
|
|
432
|
+
strokeWidth={1.5}
|
|
433
|
+
opacity={0.4 - i * 0.18}
|
|
434
|
+
>
|
|
435
|
+
<animate attributeName="r" values={`${rr + 12 + i * 16};${rr + 28 + i * 16};${rr + 12 + i * 16}`} dur={`${2 + i}s`} repeatCount="indefinite" />
|
|
436
|
+
<animate attributeName="opacity" values={`${0.5 - i * 0.15};0;${0.5 - i * 0.15}`} dur={`${2 + i}s`} repeatCount="indefinite" />
|
|
437
|
+
</circle>
|
|
438
|
+
))}
|
|
439
|
+
<circle cx={cx + 3} cy={cy + 3} r={rr} fill={frame.stroke} opacity={0.25} stroke="none" />
|
|
440
|
+
<circle
|
|
441
|
+
data-cdl-role="node-body"
|
|
442
|
+
cx={cx}
|
|
443
|
+
cy={cy}
|
|
444
|
+
r={rr}
|
|
445
|
+
fill="var(--cdl-tone-accent, #2d6a8f)"
|
|
446
|
+
fillOpacity={0.28}
|
|
447
|
+
stroke={frame.stroke}
|
|
448
|
+
strokeWidth={frame.strokeWidth}
|
|
449
|
+
/>
|
|
450
|
+
<ellipse cx={cx} cy={cy} rx={rr} ry={rr * 0.35} fill="none" stroke={frame.stroke} strokeWidth={1.2} opacity={0.6} />
|
|
451
|
+
<ellipse cx={cx} cy={cy - rr * 0.4} rx={rr * 0.88} ry={rr * 0.22} fill="none" stroke={frame.stroke} strokeWidth={1} opacity={0.5} />
|
|
452
|
+
<ellipse cx={cx} cy={cy + rr * 0.4} rx={rr * 0.88} ry={rr * 0.22} fill="none" stroke={frame.stroke} strokeWidth={1} opacity={0.5} />
|
|
453
|
+
<ellipse cx={cx} cy={cy} rx={rr * 0.35} ry={rr} fill="none" stroke={frame.stroke} strokeWidth={1.2} opacity={0.6} />
|
|
454
|
+
<ellipse cx={cx} cy={cy} rx={rr * 0.7} ry={rr} fill="none" stroke={frame.stroke} strokeWidth={1} opacity={0.4} />
|
|
455
|
+
<path d={`M ${cx - rr * 0.6} ${cy - rr * 0.1} Q ${cx - rr * 0.5} ${cy - rr * 0.35}, ${cx - rr * 0.3} ${cy - rr * 0.25} Q ${cx - rr * 0.2} ${cy}, ${cx - rr * 0.4} ${cy + rr * 0.2} Z`} fill="var(--cdl-tone-accent, #2d6a8f)" fillOpacity={0.5} stroke="none" />
|
|
456
|
+
<path d={`M ${cx + rr * 0.1} ${cy - rr * 0.3} Q ${cx + rr * 0.4} ${cy - rr * 0.4}, ${cx + rr * 0.5} ${cy - rr * 0.1} Q ${cx + rr * 0.35} ${cy + rr * 0.05}, ${cx + rr * 0.2} ${cy - rr * 0.05} Z`} fill="var(--cdl-tone-accent, #2d6a8f)" fillOpacity={0.5} stroke="none" />
|
|
457
|
+
{[
|
|
458
|
+
{ x: cx - rr * 1.05, y: cy - rr * 0.35 },
|
|
459
|
+
{ x: cx + rr * 1.05, y: cy - rr * 0.4 },
|
|
460
|
+
{ x: cx - rr * 0.85, y: cy + rr * 0.7 },
|
|
461
|
+
{ x: cx + rr * 0.9, y: cy + rr * 0.55 },
|
|
462
|
+
{ x: cx, y: cy - rr * 1.1 },
|
|
463
|
+
].map((p, i) => (
|
|
464
|
+
<g key={i}>
|
|
465
|
+
<line x1={cx} y1={cy} x2={p.x} y2={p.y} stroke={frame.stroke} strokeWidth={0.8} strokeDasharray="2 3" opacity={0.4} />
|
|
466
|
+
<circle cx={p.x} cy={p.y} r={7} fill="var(--cdl-node-fill, #ffffff)" stroke={frame.stroke} strokeWidth={2} />
|
|
467
|
+
<circle cx={p.x} cy={p.y} r={3.5} fill="#27c93f" stroke="none">
|
|
468
|
+
<animate attributeName="opacity" values="0.3;1;0.3" dur={`${1.4 + i * 0.2}s`} repeatCount="indefinite" />
|
|
469
|
+
</circle>
|
|
470
|
+
</g>
|
|
471
|
+
))}
|
|
472
|
+
<path d={`M ${cx - 8} ${cy - 14} L ${cx + 4} ${cy - 14} L ${cx - 2} ${cy - 2} L ${cx + 6} ${cy - 2} L ${cx - 4} ${cy + 14} L ${cx + 2} ${cy + 2} L ${cx - 6} ${cy + 2} Z`} fill="#e5c07b" stroke="#a67c00" strokeWidth={1.5} strokeLinejoin="round" />
|
|
473
|
+
</g>
|
|
474
|
+
);
|
|
475
|
+
return (
|
|
476
|
+
<g>
|
|
477
|
+
{fitArt(r, { 上: 広がり, 下: 広がり, 左: 広がり, 右: 広がり }, 絵)}
|
|
478
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
479
|
+
</g>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* API 一覧板 (`shape-api-gateway`) の固定部品 (#433)。
|
|
485
|
+
*
|
|
486
|
+
* 行は板の上端から固定の間隔で並んでいたため、 板を箱に合わせて縮めると行が板の外へ出た
|
|
487
|
+
* (変更前は高さ 72 の箱で下へ 74)。 **行の間隔を板の高さから出す** ので、 板がどんな高さでも
|
|
488
|
+
* 行は内側に収まる。
|
|
489
|
+
*/
|
|
490
|
+
const API_ROW_TOP = 52;
|
|
491
|
+
const API_ROW_H = 24;
|
|
492
|
+
const API_ROW_PITCH_MIN = 30;
|
|
493
|
+
const API_ROW_BOTTOM_PAD = 12;
|
|
494
|
+
const API_ROWS = 3;
|
|
495
|
+
/** 板の大きさ。 高さの下限は行が最小の間隔で 3 本入る高さ。 */
|
|
496
|
+
const API_H_MIN =
|
|
497
|
+
API_ROW_TOP + (API_ROWS - 1) * API_ROW_PITCH_MIN + API_ROW_H + API_ROW_BOTTOM_PAD;
|
|
498
|
+
const API_W_MIN = 200;
|
|
499
|
+
const API_W_MAX = 300;
|
|
500
|
+
|
|
501
|
+
/** shape-api-gateway = SaaS API panel (dark bg + method + path endpoints + status) */
|
|
502
|
+
export function ShapeApiGatewayNode({ node, active }: ShapeProps): JSX.Element {
|
|
503
|
+
const frame = commonFrame(active);
|
|
504
|
+
const r = shapeRegion(node);
|
|
505
|
+
const cx = r.cx;
|
|
506
|
+
const 余白 = Math.max(DROP_SHADOW_D, frame.strokeWidth / 2);
|
|
507
|
+
const 縦 = Math.max(r.availH, API_H_MIN + 余白 * 2);
|
|
508
|
+
const bh = 縦 - 余白 * 2;
|
|
509
|
+
const bw = Math.min(Math.max(r.availW - 余白 * 2, API_W_MIN), API_W_MAX);
|
|
510
|
+
const x = cx - bw / 2;
|
|
511
|
+
const y = r.cy - 縦 / 2 + 余白;
|
|
512
|
+
const 広がり = { 上: 縦 / 2, 下: 縦 / 2, 左: bw / 2 + 余白, 右: bw / 2 + 余白 };
|
|
513
|
+
// 行は板の中で均す。 板が伸びたら間隔も伸びる = 下に空きだけが残らない
|
|
514
|
+
const rowPitch = Math.max(
|
|
515
|
+
API_ROW_PITCH_MIN,
|
|
516
|
+
(bh - API_ROW_TOP - API_ROW_H - API_ROW_BOTTOM_PAD) / (API_ROWS - 1),
|
|
517
|
+
);
|
|
518
|
+
const 絵 = (
|
|
519
|
+
<g>
|
|
520
|
+
<rect x={x + DROP_SHADOW_D} y={y + DROP_SHADOW_D} width={bw} height={bh} rx={10} fill={frame.stroke} opacity={0.2} stroke="none" />
|
|
521
|
+
<rect data-cdl-role="node-body" x={x} y={y} width={bw} height={bh} rx={10} fill="#1a2332" stroke={frame.stroke} strokeWidth={frame.strokeWidth} />
|
|
522
|
+
<rect x={x} y={y} width={bw} height={36} rx={10} fill="var(--cdl-tone-accent, #2d6a8f)" stroke="none" />
|
|
523
|
+
<text x={cx} y={y + 24} fontSize={14} fontWeight={700} letterSpacing={3} textAnchor="middle" fill="#ffffff" fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace">
|
|
524
|
+
API GATEWAY
|
|
525
|
+
</text>
|
|
526
|
+
{[
|
|
527
|
+
{ method: "GET", path: "/users", color: "#27c93f" },
|
|
528
|
+
{ method: "POST", path: "/orders", color: "#e5c07b" },
|
|
529
|
+
{ method: "DEL", path: "/session", color: "#c9302c" },
|
|
530
|
+
].map((e, i) => (
|
|
531
|
+
<g key={i} transform={`translate(${x + 16}, ${y + API_ROW_TOP + i * rowPitch})`}>
|
|
532
|
+
<rect x={0} y={0} width={bw - 32} height={API_ROW_H} rx={4} fill="#0f1419" stroke="none" />
|
|
533
|
+
<rect x={0} y={0} width={54} height={API_ROW_H} rx={4} fill={e.color} stroke="none" />
|
|
534
|
+
<text x={27} y={16} fontSize={11} fontWeight={700} textAnchor="middle" fill="#ffffff" fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace">
|
|
535
|
+
{e.method}
|
|
536
|
+
</text>
|
|
537
|
+
<text x={64} y={16} fontSize={12} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace" fill="#e6edf3">
|
|
538
|
+
{e.path}
|
|
539
|
+
</text>
|
|
540
|
+
<circle cx={bw - 44} cy={12} r={4} fill="#27c93f" stroke="none">
|
|
541
|
+
<animate attributeName="opacity" values="0.4;1;0.4" dur={`${1.2 + i * 0.3}s`} repeatCount="indefinite" />
|
|
542
|
+
</circle>
|
|
543
|
+
</g>
|
|
544
|
+
))}
|
|
545
|
+
</g>
|
|
546
|
+
);
|
|
547
|
+
return (
|
|
548
|
+
<g>
|
|
549
|
+
{fitArt(r, 広がり, 絵)}
|
|
550
|
+
{bottomLabel(node, r.cx, r.label)}
|
|
551
|
+
</g>
|
|
552
|
+
);
|
|
553
|
+
}
|