Seen as a design The generated document itself — its toggles and disclosures work here
Seen as a design The generated document itself — its toggles and disclosures work here
Copy and paste, or save a file and hand it over. For a machine with no network, one “Everything” file is enough.
Everything in this panel, including the contents behind each URL, in one Markdown file — Large, so it goes as a file (copy is for 32 KB and under)
Build an HTML document with "Business Capability Map (Investment Gap)". This layout runs, so there is no composition to rebuild — what it needs is content.
## What this layout does
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
## What you have to supply
- The domains the capabilities sit in
- The planned effort for each capability
- Its maturity and strategic priority, both 1-5
## The shape of the input
- `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 holds the machine-checkable validation. Ask rather than guess when an input is missing.
## When not to use it
- Comparing every row-column intersection on one scale
- Decomposing cause into parent and child, or stacking values on branches
## How to build it
Put the input in a JSON file and run:
```bash
node scripts/render.mjs business-capability-map <input.json> --theme ink -o out.html
```
The output is one self-contained file and fetches nothing at render time.
## The source
- 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 Template, input schema, sample data and metadata. This document was generated from those four. The generated document
/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"
}