作品として見る 生成された文書そのもの — 切り替えも開閉もそのまま動きます
作品として見る 生成された文書そのもの — 切り替えも開閉もそのまま動きます
コピーして貼るか、ファイルに保存して渡す。ネットにつながらない環境には「全部入り」を 1 つ渡せば足りる。
このパネルの中身を、URL の先にあるファイルの中身まで含めて 1 つにまとめた Markdown — 大きいのでファイルで渡す(コピーは 32KB まで)
「ピラミッド(戦略階層)」でHTML資料を作ってください。この型は実行できるので、版面を組み直す必要はありません — 必要なのは中身です。
## この型がすること
ビジョン・施策・実行タスクを、重み比率から導出した 3 層の地層断面として示す。帯と階層レコードは同じ上下境界を共有し、対応する 1 層だけを追える
## 渡す必要があるもの
- 上位・中位・下位の 3 階層
- 各階層の重み
- 階層ごとの明細
## 入力の形
- `title` — string — スライドタイトル。結論を書くメッセージラインを推奨
- `subtitle` — string(任意)— 補足・出典など
- `tiers` — [頂点(apex), 中間(mid), 底辺(base)] の固定 3 要素。{ label, weight, note?, items? }。weight の比率でピラミッド帯と右側レコードの共通高さ・境界を自動計算。note と items は主図下の補足で展開
入力の機械可読な検証は schema.ts が持ちます。足りない入力は推測せず質問してください。
## 向かない場合
- 枝ごとに深さが異なる可変階層を分解したい場合
- 時間順や責任分担を説明したい場合
## 作り方
入力を JSON にまとめ、次を実行します。
```bash
node scripts/render.mjs pyramid <入力.json> --theme ink -o out.html
```
出力は 1 ファイルで完結し、外部から何も読み込みません。
## 原本
- https://design-vault.pages.dev/designs/pyramid/source/template.tsx
- https://design-vault.pages.dev/designs/pyramid/source/schema.ts
- https://design-vault.pages.dev/designs/pyramid/source/sample-data.json
- https://design-vault.pages.dev/designs/pyramid/sample.html テンプレート・入力スキーマ・サンプルデータ・メタ情報。この文書はこの 4 つから生成されています。 生成された文書
/designs/pyramid/source/template.tsx
import type { PyramidProps } from "./schema";
/** 重みを高さ比率(%)に正規化。全重み0以下の退化ケースは均等割りにする */
export const normalizeWeights = (tiers: { weight: number }[]): number[] => {
const total = tiers.reduce((s, t) => s + Math.max(0, t.weight), 0);
if (total <= 0) return tiers.map(() => 100 / tiers.length);
return tiers.map((t) => (Math.max(0, t.weight) / total) * 100);
};
/**
* 各帯の頂点からの累積位置(%)。真の三角形では「頂点からの垂直位置」と
* 「その高さでのテーパー幅」が同じ比率になるため、topWidthPct/bottomWidthPct
* は帯の縦位置(top/height)と横のテーパー(clip-path)の両方に使い回せる。
*/
export const pyramidBands = (
tiers: { weight: number }[],
): { heightPct: number; topWidthPct: number; bottomWidthPct: number }[] => {
const heights = normalizeWeights(tiers);
let acc = 0;
return heights.map((h) => {
const topWidthPct = acc;
acc += h;
return { heightPct: h, topWidthPct, bottomWidthPct: acc };
});
};
/** 頂点→中間→底辺の固定indexを、帯・ガイド・ledgerの共有identityへ使う */
const TIER_STYLE = [
{
band: "bg-tertiary",
record: "text-tertiary",
guide: "border-tertiary/55",
role: "頂点",
},
{
band: "bg-tertiary-container",
record: "text-secondary",
guide: "border-outline",
role: "重点",
},
{
band: "bg-neutral",
record: "text-secondary",
guide: "border-outline",
role: "基盤",
},
];
/** エントランスの時差。間隔は global.css の --stagger-step で統一 */
const stagger = (i: number, baseMs = 0) =>
`calc(${baseMs}ms + ${i} * var(--stagger-step))`;
export default function Pyramid({ title, subtitle, tiers }: PyramidProps) {
const bands = pyramidBands(tiers);
const notes = tiers.filter(
(tier) => tier.note || (tier.items && tier.items.length > 0),
);
return (
<section className="mx-auto max-w-slide bg-surface p-6 font-sans text-on-surface sm:p-12">
<header className="mb-8">
<h1 className="text-h1">{title}</h1>
{subtitle && (
<p className="mt-2 text-body-sm text-secondary">{subtitle}</p>
)}
</header>
<style>{`
[data-pyramid-map] [data-pyramid-band],
[data-pyramid-map] [data-pyramid-record],
[data-pyramid-map] [data-pyramid-guide] {
transition:
color 140ms ease,
background-color 140ms ease,
border-color 140ms ease,
opacity 140ms ease;
}
[data-pyramid-map]:has([data-pyramid-hotspot="0"]:is(:hover, :focus))
[data-pyramid-band="0"],
[data-pyramid-map]:has([data-pyramid-hotspot="1"]:is(:hover, :focus))
[data-pyramid-band="1"],
[data-pyramid-map]:has([data-pyramid-hotspot="2"]:is(:hover, :focus))
[data-pyramid-band="2"] {
background-color: var(--color-on-surface);
opacity: 1;
}
[data-pyramid-map]:has([data-pyramid-hotspot="0"]:is(:hover, :focus))
[data-pyramid-record="0"],
[data-pyramid-map]:has([data-pyramid-hotspot="1"]:is(:hover, :focus))
[data-pyramid-record="1"],
[data-pyramid-map]:has([data-pyramid-hotspot="2"]:is(:hover, :focus))
[data-pyramid-record="2"] {
color: var(--color-on-surface);
opacity: 1;
}
[data-pyramid-map]:has([data-pyramid-hotspot="0"]:is(:hover, :focus))
[data-pyramid-guide="0"],
[data-pyramid-map]:has([data-pyramid-hotspot="1"]:is(:hover, :focus))
[data-pyramid-guide="1"],
[data-pyramid-map]:has([data-pyramid-hotspot="2"]:is(:hover, :focus))
[data-pyramid-guide="2"] {
border-color: var(--color-on-surface);
opacity: 1;
}
[data-pyramid-map]:has([data-pyramid-hotspot="1"]:is(:hover, :focus))
:is([data-pyramid-band="0"], [data-pyramid-record="0"], [data-pyramid-guide="0"]),
[data-pyramid-map]:has([data-pyramid-hotspot="2"]:is(:hover, :focus))
:is([data-pyramid-band="0"], [data-pyramid-record="0"], [data-pyramid-guide="0"]) {
opacity: 0.34;
}
`}</style>
{/*
* 一つの重み付き座標系を左右で共有する「地層断面」。
* 左の三角帯と右の階層ledgerは同じ top/height を使い、
* mobileでも上下へ分離せず対応関係を保つ。
*/}
<div
data-pyramid-map="true"
className="grid h-80 grid-cols-[4.5rem_minmax(0,1fr)] gap-x-4 sm:h-96 sm:grid-cols-[16rem_minmax(0,1fr)] sm:gap-x-8"
>
<div className="relative h-full">
{tiers.map((_, i) => {
const band = bands[i];
const clipPath = `polygon(${50 - band.topWidthPct / 2}% 0%, ${50 + band.topWidthPct / 2}% 0%, ${50 + band.bottomWidthPct / 2}% 100%, ${50 - band.bottomWidthPct / 2}% 100%)`;
return (
<div key={i}>
<button
type="button"
data-pyramid-band={i}
data-pyramid-hotspot={i}
data-top-pct={band.topWidthPct}
data-height-pct={band.heightPct}
aria-label={`${tiers[i].label}、配分${Math.round(band.heightPct)}%`}
className={`absolute inset-x-0 cursor-default focus-visible:outline-none ${TIER_STYLE[i].band}`}
style={{
top: `${band.topWidthPct}%`,
height: `${band.heightPct}%`,
clipPath,
}}
/>
<span
data-pyramid-guide={i}
aria-hidden="true"
className={`pointer-events-none absolute right-0 border-t ${TIER_STYLE[i].guide}`}
style={{
top: `${band.topWidthPct}%`,
left: `${50 + band.topWidthPct / 2}%`,
}}
/>
</div>
);
})}
</div>
<div className="relative h-full min-w-0">
{tiers.map((tier, i) => (
<button
type="button"
key={i}
data-pyramid-record={i}
data-pyramid-hotspot={i}
data-top-pct={bands[i].topWidthPct}
data-height-pct={bands[i].heightPct}
className={`absolute inset-x-0 grid min-h-0 cursor-default grid-cols-[minmax(0,1fr)_auto] items-center gap-2 border-t pl-2 pr-1 text-left focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-on-surface sm:gap-4 sm:pl-4 sm:pr-3 ${i === tiers.length - 1 ? "border-b" : ""} ${TIER_STYLE[i].record} ${TIER_STYLE[i].guide}`}
style={{
top: `${bands[i].topWidthPct}%`,
height: `${bands[i].heightPct}%`,
}}
>
<span
aria-hidden="true"
className={`pointer-events-none absolute right-full top-0 w-4 border-t sm:w-8 ${TIER_STYLE[i].guide}`}
/>
<span
className="animate-rise flex min-w-0 items-baseline gap-3"
style={{ animationDelay: stagger(i, 150) }}
>
<span className="hidden shrink-0 font-mono text-label opacity-60 sm:inline">
{TIER_STYLE[i].role}
</span>
<span className="min-w-0 truncate text-label sm:text-h3">
{tier.label}
</span>
</span>
<span className="font-mono text-label tabular-nums">
{Math.round(bands[i].heightPct)}%
</span>
</button>
))}
</div>
</div>
{notes.length > 0 && (
<details
data-inline-disclosure="true"
className="group mt-6 border-y border-outline bg-surface-subtle"
>
<summary className="flex cursor-pointer list-none items-center px-4 py-3 text-label text-secondary focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tertiary">
<span>階層の補足を確認</span>
<span className="ml-auto font-mono">{notes.length}層</span>
<span
aria-hidden="true"
className="ml-2 text-tertiary transition-transform group-open:rotate-45"
>
+
</span>
</summary>
<div className="border-t border-outline px-4">
{notes.map((tier) => {
const index = tiers.indexOf(tier);
return (
<article
key={index}
data-note-item="true"
className="grid gap-2 border-b border-outline py-4 last:border-b-0 sm:grid-cols-[11rem_minmax(0,1fr)] sm:gap-6"
>
<div>
<h2 className="text-label">{tier.label}</h2>
<p className="mt-1 font-mono text-label text-secondary">
{TIER_STYLE[index].role} ・ 配分{" "}
{Math.round(bands[index].heightPct)}%
</p>
</div>
<div className="min-w-0">
{tier.note && (
<p className="text-body-sm">{tier.note}</p>
)}
{tier.items && tier.items.length > 0 && (
<ul className="mt-2 grid gap-x-6 gap-y-1 text-body-sm sm:grid-cols-2">
{tier.items.map((item, itemIndex) => (
<li key={itemIndex} className="flex gap-2">
<span aria-hidden="true" className="text-tertiary">
—
</span>
<span>{item}</span>
</li>
))}
</ul>
)}
</div>
</article>
);
})}
</div>
</details>
)}
</section>
);
}
/designs/pyramid/source/schema.ts
import { z } from "zod";
export const pyramidTierSchema = z
.object({
label: z.string(),
weight: z
.number()
.positive()
.describe(
"この階層の重み(予算配分比率・工数比率など)。帯の高さ配分に使う。" +
"正の数のみ —— 0 と負値は正規化で高さ 0% になり、schema が 3 層必須と" +
"定めているのに 1 層がラベルごと消える",
),
note: z.string().optional().describe("階層の補足説明"),
items: z
.array(z.string())
.optional()
.describe("details で展開する明細(施策・タスク等)"),
})
.strict();
export const schema = z
.object({
title: z.string().describe("スライドタイトル。結論を書くメッセージラインを推奨"),
subtitle: z.string().optional().describe("補足・出典など"),
tiers: z
.tuple([pyramidTierSchema, pyramidTierSchema, pyramidTierSchema])
.describe("頂点(apex)→中間(mid)→底辺(base) の固定3階層"),
})
.strict();
export type PyramidTier = z.infer<typeof pyramidTierSchema>;
export type PyramidProps = z.infer<typeof schema>;
/designs/pyramid/source/meta.yaml
id: pyramid
name:
ja: "ピラミッド(戦略階層)"
en: "Pyramid (Strategy Hierarchy)"
purpose:
ja: "ビジョン・施策・実行タスクを、重み比率から導出した 3 層の地層断面として示す。帯と階層レコードは同じ上下境界を共有し、対応する 1 層だけを追える"
en: "Vision, initiatives and tasks as three strata whose depths come from their weights. The band and the records share the same boundaries, so one layer can be followed on its own"
status: published
# 索引カードの図版。pnpm layout:render が原本から描く。
still: ./still.png
category: hierarchy
catalogOrder: 130
jobs:
- explain-structure
useCases:
- summary
layoutKinds:
- stack
tags: [pyramid, hierarchy, strategy, data-driven, interactive]
whenToUse:
- ja: "全社戦略・中期計画のビジョンから実行タスクへの落とし込み"
en: "Taking a strategy from its vision down to the work it implies"
- ja: "組織階層や意思決定プロセスの整理"
en: "Setting out an org hierarchy or a decision process"
- ja: "優先順位が上位ほど絞り込まれる構造の提示"
en: "Showing a structure that narrows as it rises"
avoidWhen:
- ja: "枝ごとに深さが異なる可変階層を分解したい場合"
en: "Decomposing a hierarchy whose branches differ in depth"
- ja: "時間順や責任分担を説明したい場合"
en: "Explaining an order in time, or who is responsible"
requiredInputs:
- ja: "上位・中位・下位の 3 階層"
en: "Three tiers: apex, middle and base"
- ja: "各階層の重み"
en: "A weight for each tier"
- ja: "階層ごとの明細"
en: "What sits in each tier"
relatedLayouts: [logic-tree, process-flow, matrix-2x2]
dataSchema:
title: "string — スライドタイトル。結論を書くメッセージラインを推奨"
subtitle: "string(任意)— 補足・出典など"
tiers: "[頂点(apex), 中間(mid), 底辺(base)] の固定 3 要素。{ label, weight, note?, items? }。weight の比率でピラミッド帯と右側レコードの共通高さ・境界を自動計算。note と items は主図下の補足で展開"
sources: []
/designs/pyramid/source/sample-data.json
{
"title": "全社DX戦略ピラミッド — ビジョンから実行タスクまで一気通貫",
"subtitle": "帯の高さは予算配分比率に比例(2026年度 全社DX戦略・サンプル)",
"tiers": [
{
"label": "全社ビジョン",
"weight": 10,
"note": "「データで意思決定する会社になる」を3年後の到達点とする",
"items": [
"経営会議への月次データレポート提出を必須化",
"全社KPIダッシュボードの経営陣閲覧率100%"
]
},
{
"label": "重点施策(今期)",
"weight": 30,
"note": "ビジョン実現に向けた今期の重点テーマ3本",
"items": [
"基幹データ基盤の統合",
"部門横断KPIの定義統一",
"意思決定会議へのデータ活用ルール導入"
]
},
{
"label": "実行タスク",
"weight": 60,
"note": "各部門が今期着手する具体タスク群",
"items": [
"営業: 案件データの一元化",
"経理: 月次レポート自動化",
"人事: 稼働データの可視化",
"IT: データ基盤のセキュリティ強化"
]
}
]
}