作品として見る 生成された文書そのもの — 切り替えも開閉もそのまま動きます
作品として見る 生成された文書そのもの — 切り替えも開閉もそのまま動きます
コピーして貼るか、ファイルに保存して渡す。ネットにつながらない環境には「全部入り」を 1 つ渡せば足りる。
このパネルの中身を、URL の先にあるファイルの中身まで含めて 1 つにまとめた Markdown — 大きいのでファイルで渡す(コピーは 32KB まで)
「ビジネス能力マップ(投資ギャップ)」でHTML資料を作ってください。この型は実行できるので、版面を組み直す必要はありません — 必要なのは中身です。
## この型がすること
事業ドメインの内側へ能力を包含し、領域内工数比率を区画幅、成熟度を面、戦略優先度を境界へ写す。成熟度不足と優先度から最大投資ギャップを1件導出し、能力マップと根拠ledgerを同じidentityで切り替えて追える
## 渡す必要があるもの
- 能力を包含する事業・業務ドメイン
- 各能力の計画工数
- 各能力の成熟度と戦略優先度(ともに1〜5)
## 入力の形
- `title` — string — 最大投資ギャップを結論として書く
- `subtitle` — string(任意)— 評価時点・対象範囲・尺度・出典など
- `effortUnit` — string — 計画工数の単位(人月、ptなど)
- `domains` — { name, capabilities }[] — 能力を包含する事業・業務ドメイン(2〜5件)
- `domains[].capabilities` — { name, effort, maturity, strategicPriority, note? }[] — 各2〜5件。effortは正数、maturityとstrategicPriorityは1〜5整数
入力の機械可読な検証は schema.ts が持ちます。足りない入力は推測せず質問してください。
## 向かない場合
- 行と列の全交点を同じ尺度で比較したい場合
- 親子の因果分解や値の積み上げを枝として示したい場合
## 作り方
入力を JSON にまとめ、次を実行します。
```bash
node scripts/render.mjs business-capability-map <入力.json> --theme ink -o out.html
```
出力は 1 ファイルで完結し、外部から何も読み込みません。
## 原本
- https://design-vault.pages.dev/designs/business-capability-map/source/template.tsx
- https://design-vault.pages.dev/designs/business-capability-map/source/schema.ts
- https://design-vault.pages.dev/designs/business-capability-map/source/sample-data.json
- https://design-vault.pages.dev/designs/business-capability-map/sample.html テンプレート・入力スキーマ・サンプルデータ・メタ情報。この文書はこの 4 つから生成されています。 生成された文書
/designs/business-capability-map/source/template.tsx
import { useId } from "react";
import type {
BusinessCapability,
BusinessCapabilityMapProps,
CapabilityDomain,
} from "./schema";
export type FlatCapability = BusinessCapability & {
domainName: string;
domainIndex: number;
capabilityIndex: number;
originalIndex: number;
identity: string;
coordinate: string;
gap: number;
widthPercent: number;
};
export function capabilityGap(
capability: Pick<BusinessCapability, "maturity" | "strategicPriority">,
) {
return capability.strategicPriority * (6 - capability.maturity);
}
export function capabilityIdentity(domainIndex: number, capabilityIndex: number) {
return `d${domainIndex}-c${capabilityIndex}`;
}
export function capabilityCoordinate(
domainIndex: number,
capabilityIndex: number,
) {
return `D${String(domainIndex + 1).padStart(2, "0")} · C${String(
capabilityIndex + 1,
).padStart(2, "0")}`;
}
export function capabilityWidthPercent(
capability: Pick<BusinessCapability, "effort">,
capabilities: readonly Pick<BusinessCapability, "effort">[],
) {
const total = capabilities.reduce((sum, item) => sum + item.effort, 0);
return total > 0 ? (capability.effort / total) * 100 : 0;
}
export function flattenCapabilities(
domains: readonly CapabilityDomain[],
): FlatCapability[] {
let originalIndex = 0;
return domains.flatMap((domain, domainIndex) =>
domain.capabilities.map((capability, capabilityIndex) => ({
...capability,
domainName: domain.name,
domainIndex,
capabilityIndex,
originalIndex: originalIndex++,
identity: capabilityIdentity(domainIndex, capabilityIndex),
coordinate: capabilityCoordinate(domainIndex, capabilityIndex),
gap: capabilityGap(capability),
widthPercent: capabilityWidthPercent(capability, domain.capabilities),
})),
);
}
export function rankedCapabilities(domains: readonly CapabilityDomain[]) {
return flattenCapabilities(domains).sort(
(a, b) => b.gap - a.gap || a.originalIndex - b.originalIndex,
);
}
export function priorityCapability(domains: readonly CapabilityDomain[]) {
return rankedCapabilities(domains)[0] ?? null;
}
const MATURITY_CLASSES = [
"bg-surface-subtle",
"bg-tertiary/10",
"bg-tertiary/20",
"bg-tertiary/30",
"bg-tertiary/40",
] as const;
const PRIORITY_CLASSES = [
"border-l-outline",
"border-l-primary/30",
"border-l-primary/50",
"border-l-primary/70",
"border-l-primary",
] as const;
function metricLabel(capability: BusinessCapability, effortUnit: string) {
return `${capability.effort}${effortUnit} · M${capability.maturity} · P${capability.strategicPriority}`;
}
function capabilitySurfaceClass(capability: FlatCapability, priority: boolean) {
if (priority) {
return "border-tertiary bg-gradient-to-br from-tertiary to-accent-violet text-on-primary shadow-card";
}
return `border-outline ${MATURITY_CLASSES[capability.maturity - 1]} ${PRIORITY_CLASSES[capability.strategicPriority - 1]} text-on-surface`;
}
export default function BusinessCapabilityMap({
title,
subtitle,
effortUnit,
domains,
instanceId,
}: BusinessCapabilityMapProps) {
const autoId = useId();
const uid =
(instanceId ?? autoId).replace(
// 捨てる操作は単射でないので、schema を通る別々の値が同じ id になる
// (`sales:a` と `sales/a` がどちらも `salesa`)。同じ文書へ 2 つ置くと
// id と radio の name が衝突して、片方の操作がもう片方に効く。
// 逃がし文字(`_`)自身も逃がすので、この変換は単射。
/[^a-zA-Z0-9-]/g,
(c) => `_${c.charCodeAt(0).toString(16)}_`,
) || "capability-map";
const radioName = `capability-view-${uid}`;
const capabilities = flattenCapabilities(domains);
const ranked = rankedCapabilities(domains);
const priority = priorityCapability(domains);
const notes = capabilities.filter((capability) => capability.note);
const interactionCss = capabilities
.map(
(capability) => `
[data-capability-root="${uid}"]:has([data-capability-hotspot="${capability.identity}"]:is(:hover,:focus-visible)) [data-capability-surface="${capability.identity}"] {
border-color: var(--color-primary);
box-shadow: inset .2rem 0 0 var(--color-primary);
filter: none;
}
[data-capability-root="${uid}"]:has([data-capability-hotspot="${capability.identity}"]:is(:hover,:focus-visible)) [data-capability-default-priority="true"]:not([data-capability-surface="${capability.identity}"]) {
filter: saturate(.3) brightness(1.08);
}`,
)
.join("\n");
return (
<section
data-capability-root={uid}
className="group/capability mx-auto max-w-slide bg-surface p-12 font-sans text-on-surface"
>
<style>{interactionCss}</style>
<header className="mb-6 flex flex-wrap items-end justify-between gap-4">
<div className="max-w-4xl">
<p className="mb-2 font-mono text-label text-tertiary">
BUSINESS CAPABILITY PORTFOLIO
</p>
<h1 className="text-h1">{title}</h1>
{subtitle && (
<p className="mt-2 text-body-sm text-secondary">{subtitle}</p>
)}
</div>
<div className="flex shrink-0 rounded-sm border border-outline bg-surface-subtle p-1 shadow-card">
<input
id={`${uid}-map`}
type="radio"
name={radioName}
defaultChecked
className="capability-view-map sr-only"
/>
<label
htmlFor={`${uid}-map`}
className="cursor-pointer rounded-sm px-4 py-1.5 text-label text-secondary group-has-[.capability-view-map:checked]/capability:bg-primary group-has-[.capability-view-map:checked]/capability:text-on-primary group-has-[.capability-view-map:focus-visible]/capability:ring-2 group-has-[.capability-view-map:focus-visible]/capability:ring-tertiary"
>
能力マップ
</label>
<input
id={`${uid}-gaps`}
type="radio"
name={radioName}
className="capability-view-gaps sr-only"
/>
<label
htmlFor={`${uid}-gaps`}
className="cursor-pointer rounded-sm px-4 py-1.5 text-label text-secondary group-has-[.capability-view-gaps:checked]/capability:bg-primary group-has-[.capability-view-gaps:checked]/capability:text-on-primary group-has-[.capability-view-gaps:focus-visible]/capability:ring-2 group-has-[.capability-view-gaps:focus-visible]/capability:ring-tertiary"
>
投資ギャップ
</label>
</div>
</header>
{priority && (
<div className="mb-6 grid gap-3 border-y border-outline py-4 sm:grid-cols-[auto_minmax(0,1fr)_auto] sm:items-center sm:gap-6">
<div>
<p className="font-mono text-label text-tertiary">PRIORITY GAP</p>
<p className="mt-0.5 font-mono text-h3">{priority.coordinate}</p>
</div>
<div className="min-w-0">
<p className="text-label text-secondary">{priority.domainName}</p>
<p className="font-semibold">{priority.name}</p>
</div>
<div className="sm:text-right">
<p className="font-mono text-h2">{priority.gap}</p>
<p className="font-mono text-label text-secondary">
P{priority.strategicPriority} × (6 − M{priority.maturity})
</p>
</div>
</div>
)}
<div className="block group-has-[.capability-view-gaps:checked]/capability:hidden">
<div
data-capability-view="map-desktop"
className="hidden space-y-3 sm:block"
>
{domains.map((domain, domainIndex) => {
const domainCapabilities = capabilities.filter(
(capability) => capability.domainIndex === domainIndex,
);
const domainEffort = domain.capabilities.reduce(
(sum, capability) => sum + capability.effort,
0,
);
return (
<div
key={domain.name}
data-capability-domain={domainIndex}
className="grid grid-cols-[10rem_minmax(0,1fr)] border-y border-outline"
>
<div className="flex flex-col justify-between border-r border-outline bg-primary px-4 py-4 text-on-primary">
<div>
<p className="font-mono text-label text-on-primary/60">
D{String(domainIndex + 1).padStart(2, "0")}
</p>
<h2 className="mt-1 text-h3">{domain.name}</h2>
</div>
<p className="mt-4 font-mono text-label text-on-primary/70">
{domainEffort} {effortUnit}
</p>
</div>
<div className="flex min-w-0 gap-1 bg-outline p-1">
{domainCapabilities.map((capability) => {
const isPriority = capability.identity === priority?.identity;
return (
<article
key={capability.identity}
tabIndex={0}
data-capability-id={capability.identity}
data-capability-hotspot={capability.identity}
data-capability-surface={capability.identity}
data-capability-default-priority={
isPriority ? "true" : undefined
}
data-capability-gap={capability.gap}
data-capability-width={capability.widthPercent.toFixed(2)}
className={`min-w-0 border border-l-4 p-3 outline-none transition-[border-color,box-shadow,filter] duration-fast focus-visible:ring-2 focus-visible:ring-tertiary ${capabilitySurfaceClass(capability, isPriority)}`}
style={{
flexBasis: `${capability.widthPercent}%`,
flexGrow: capability.widthPercent,
}}
aria-label={`${domain.name}、${capability.name}、${metricLabel(capability, effortUnit)}`}
>
<p className="font-mono text-label opacity-70">
{capability.coordinate}
</p>
<p className="mt-2 break-words font-semibold leading-snug">
{capability.name}
</p>
<p className="mt-3 font-mono text-label opacity-80">
{metricLabel(capability, effortUnit)}
</p>
</article>
);
})}
</div>
</div>
);
})}
</div>
<div
data-capability-view="map-mobile"
className="space-y-5 sm:hidden"
>
{domains.map((domain, domainIndex) => (
<section key={domain.name} data-capability-mobile-domain={domainIndex}>
<div className="mb-2 flex items-baseline justify-between gap-3 border-b border-outline pb-2">
<h2 className="text-h3">{domain.name}</h2>
<span className="font-mono text-label text-secondary">
D{String(domainIndex + 1).padStart(2, "0")}
</span>
</div>
<div className="space-y-1">
{capabilities
.filter((capability) => capability.domainIndex === domainIndex)
.map((capability) => {
const isPriority = capability.identity === priority?.identity;
return (
<article
key={capability.identity}
tabIndex={0}
data-capability-id={capability.identity}
data-capability-hotspot={capability.identity}
data-capability-surface={capability.identity}
data-capability-default-priority={
isPriority ? "true" : undefined
}
data-capability-gap={capability.gap}
className={`grid min-w-0 grid-cols-[minmax(0,1fr)_auto] gap-3 border border-l-4 p-3 outline-none transition-[border-color,box-shadow,filter] duration-fast focus-visible:ring-2 focus-visible:ring-tertiary ${capabilitySurfaceClass(capability, isPriority)}`}
>
<div className="min-w-0">
<p className="break-words font-semibold">
{capability.name}
</p>
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-outline/60">
<span
className="block h-full bg-current opacity-50"
style={{ width: `${capability.widthPercent}%` }}
/>
</div>
</div>
<div className="text-right font-mono text-label">
<p>{capability.effort}{effortUnit}</p>
<p className="mt-1 opacity-70">
M{capability.maturity} · P{capability.strategicPriority} · G{capability.gap}
</p>
</div>
</article>
);
})}
</div>
</section>
))}
</div>
</div>
<div
data-capability-view="gap-ledger"
className="hidden group-has-[.capability-view-gaps:checked]/capability:block"
>
<div className="mb-3 grid grid-cols-[3rem_minmax(0,1fr)_auto] gap-3 border-b border-outline pb-2 font-mono text-label text-secondary">
<span>RANK</span>
<span>CAPABILITY / EVIDENCE</span>
<span>GAP</span>
</div>
<div className="divide-y divide-outline border-b border-outline">
{ranked.map((capability, index) => {
const isPriority = capability.identity === priority?.identity;
return (
<article
key={capability.identity}
tabIndex={0}
data-capability-id={capability.identity}
data-capability-hotspot={capability.identity}
data-capability-surface={capability.identity}
data-capability-default-priority={isPriority ? "true" : undefined}
data-capability-gap={capability.gap}
className={`grid min-w-0 grid-cols-[3rem_minmax(0,1fr)_auto] items-center gap-3 border-l-4 px-2 py-3 outline-none transition-[border-color,box-shadow,filter] duration-fast focus-visible:ring-2 focus-visible:ring-tertiary ${isPriority ? "border-l-tertiary bg-gradient-to-r from-tertiary/20 to-transparent" : "border-l-transparent"}`}
>
<span className="font-mono text-label text-secondary">
{String(index + 1).padStart(2, "0")}
</span>
<div className="min-w-0">
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<p className="font-semibold">{capability.name}</p>
<p className="text-label text-secondary">
{capability.domainName} · {capability.coordinate}
</p>
</div>
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-outline/60">
<span
className={isPriority ? "block h-full bg-gradient-to-r from-tertiary to-accent-violet" : "block h-full bg-primary"}
style={{ width: `${(capability.gap / 25) * 100}%` }}
/>
</div>
<p className="mt-2 font-mono text-label text-secondary">
{capability.effort}{effortUnit} · 成熟度 M{capability.maturity} · 優先度 P{capability.strategicPriority}
</p>
</div>
<div className="text-right">
<p className="font-mono text-h3">{capability.gap}</p>
<p className="font-mono text-label text-secondary">/ 25</p>
</div>
</article>
);
})}
</div>
</div>
<div className="mt-6 flex flex-wrap items-center gap-x-6 gap-y-2 border-t border-outline pt-4 text-label text-secondary">
<span className="font-mono">M = 成熟度</span>
<span className="font-mono">P = 戦略優先度</span>
<span className="font-mono">G = P × (6 − M)</span>
<span>区画幅 = ドメイン内の{effortUnit}構成比</span>
</div>
{notes.length > 0 && (
<details
data-inline-disclosure="true"
className="mt-6 border border-outline bg-surface-subtle"
>
<summary className="cursor-pointer px-4 py-3 font-semibold marker:text-tertiary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tertiary">
補足を確認({notes.length}件)
</summary>
<div className="grid gap-3 border-t border-outline p-4 sm:grid-cols-2">
{notes.map((capability) => (
<article
key={capability.identity}
data-note-item="true"
className="border-l-2 border-tertiary pl-3"
>
<p className="text-label text-secondary">
{capability.domainName} · {capability.coordinate}
</p>
<p className="mt-1 font-semibold">{capability.name}</p>
<p className="mt-1 text-body-sm text-secondary">
{capability.note}
</p>
</article>
))}
</div>
</details>
)}
</section>
);
}
/designs/business-capability-map/source/schema.ts
import { z } from "zod";
export const businessCapabilitySchema = z
.object({
name: z.string().min(1).describe("能力名"),
effort: z
.number()
.positive()
.describe("同じドメイン内の能力区画幅へ反映する計画工数"),
maturity: z
.number()
.int()
.min(1)
.max(5)
.describe("現在の成熟度(1〜5。5が最も成熟)"),
strategicPriority: z
.number()
.int()
.min(1)
.max(5)
.describe("戦略優先度(1〜5。5が最優先)"),
note: z
.string()
.optional()
.describe("図の下の補足欄で明示的に展開する評価根拠"),
})
.strict();
export const capabilityDomainSchema = z
.object({
name: z.string().min(1).describe("事業・業務ドメイン名"),
capabilities: z
.array(businessCapabilitySchema)
.min(2)
.max(5)
.describe("ドメインに含まれる能力(2〜5件)"),
})
.strict();
export const schema = z
.object({
title: z.string().min(1).describe("最大投資ギャップを結論として書くタイトル"),
subtitle: z
.string()
.optional()
.describe("評価時点、対象範囲、尺度、出典など"),
effortUnit: z.string().min(1).describe("計画工数の単位(例: 人月、pt)"),
domains: z
.array(capabilityDomainSchema)
.min(2)
.max(5)
.describe("能力を包含する事業・業務ドメイン(2〜5件)"),
instanceId: z
.string()
.optional()
.describe("同一ページに複数置く場合のビュー切替radio名の識別子"),
})
.strict();
export type BusinessCapability = z.infer<typeof businessCapabilitySchema>;
export type CapabilityDomain = z.infer<typeof capabilityDomainSchema>;
export type BusinessCapabilityMapProps = z.infer<typeof schema>;
/designs/business-capability-map/source/meta.yaml
id: business-capability-map
name:
ja: "ビジネス能力マップ(投資ギャップ)"
en: "Business Capability Map (Investment Gap)"
purpose:
ja: "事業ドメインの内側へ能力を包含し、領域内工数比率を区画幅、成熟度を面、戦略優先度を境界へ写す。成熟度不足と優先度から最大投資ギャップを1件導出し、能力マップと根拠ledgerを同じidentityで切り替えて追える"
en: "Capabilities nested inside business domains, with effort share as width, maturity as fill and strategic priority at the border. The largest investment gap follows from maturity against priority"
status: published
# 索引カードの図版。pnpm layout:render が原本から描く。
still: ./still.png
category: portfolio
catalogOrder: 140
jobs:
- prioritize-diagnose
- explain-structure
useCases:
- org
layoutKinds:
- grid
tags: [capability-map, enterprise-architecture, portfolio, maturity, investment, data-driven, interactive]
whenToUse:
- ja: "全社・事業部の能力ポートフォリオと重点投資領域を一画面で合意したい場合"
en: "Agreeing a capability portfolio and where to invest, on one screen"
- ja: "現在成熟度と戦略重要度の差から、能力強化の着手順を説明したい場合"
en: "Explaining the order to strengthen capabilities, from maturity against strategic weight"
- ja: "EA・業務改革・中期IT投資でドメイン内の工数構成も同時に確認したい場合"
en: "Seeing the effort mix inside a domain alongside an EA or investment discussion"
avoidWhen:
- ja: "行と列の全交点を同じ尺度で比較したい場合"
en: "Comparing every row-column intersection on one scale"
- ja: "親子の因果分解や値の積み上げを枝として示したい場合"
en: "Decomposing cause into parent and child, or stacking values on branches"
requiredInputs:
- ja: "能力を包含する事業・業務ドメイン"
en: "The domains the capabilities sit in"
- ja: "各能力の計画工数"
en: "The planned effort for each capability"
- ja: "各能力の成熟度と戦略優先度(ともに1〜5)"
en: "Its maturity and strategic priority, both 1-5"
relatedLayouts: [heatmap, logic-tree, kpi-cards]
dataSchema:
title: "string — 最大投資ギャップを結論として書く"
subtitle: "string(任意)— 評価時点・対象範囲・尺度・出典など"
effortUnit: "string — 計画工数の単位(人月、ptなど)"
domains: "{ name, capabilities }[] — 能力を包含する事業・業務ドメイン(2〜5件)"
domains[].capabilities: "{ name, effort, maturity, strategicPriority, note? }[] — 各2〜5件。effortは正数、maturityとstrategicPriorityは1〜5整数"
sources: []
/designs/business-capability-map/source/sample-data.json
{
"title": "能力投資の最優先は顧客データ統合 — 戦略重要度5・成熟度1の差を埋める",
"subtitle": "次期中期計画に向けた能力仮説評価(成熟度・戦略優先度は5段階、工数は計画人月)",
"effortUnit": "人月",
"domains": [
{
"name": "顧客接点",
"capabilities": [
{
"name": "顧客データ統合",
"effort": 12,
"maturity": 1,
"strategicPriority": 5,
"note": "顧客IDが販売・サポート・契約管理で分かれ、横断集計に月20時間を要する想定"
},
{
"name": "マーケティング自動化",
"effort": 8,
"maturity": 3,
"strategicPriority": 4
},
{
"name": "提案・商談管理",
"effort": 10,
"maturity": 4,
"strategicPriority": 4
}
]
},
{
"name": "供給・提供",
"capabilities": [
{
"name": "需要予測",
"effort": 9,
"maturity": 2,
"strategicPriority": 5,
"note": "月次予測から週次予測へ短縮するため、販売実績と販促予定の統合が必要"
},
{
"name": "在庫最適化",
"effort": 12,
"maturity": 3,
"strategicPriority": 5
},
{
"name": "仕入先連携",
"effort": 9,
"maturity": 4,
"strategicPriority": 3
}
]
},
{
"name": "経営基盤",
"capabilities": [
{
"name": "管理会計",
"effort": 10,
"maturity": 2,
"strategicPriority": 4,
"note": "製品別採算の確定を翌月第8営業日から第5営業日へ前倒しする想定"
},
{
"name": "要員計画",
"effort": 8,
"maturity": 3,
"strategicPriority": 3
},
{
"name": "データガバナンス",
"effort": 12,
"maturity": 2,
"strategicPriority": 5,
"note": "重要データ項目のownerと品質基準を定義し、変更時の影響確認を標準化する想定"
}
]
}
],
"instanceId": "enterprise-capability-portfolio"
}