Back to index

OKR Progress

Stack
Generates documents
OKR Progress

Seen as a design

Take it with you

Copy and paste, or save a file and hand it over. For a machine with no network, one “Everything” file is enough.

Everything

okr-progress-everything.md

Everything in this panel, including the contents behind each URL, in one Markdown file

Prompt

1.3 KB
Build an HTML document with "OKR Progress". This layout runs, so there is no composition to rebuild — what it needs is content.

## What this layout does
OKR Progress with explicit source values and reading conditions.

## What you have to supply
- Provide the source values, labels and decision conditions described by the schema.

## The shape of the input
- `title` — string — 結論を伝える見出し
- `subtitle` — string? — 出典・時点・母集団や仮定
- `objectives` — array (label, owner, results) — 目標と2〜4件の成果指標。開始と目標は異なる値

schema.ts holds the machine-checkable validation. Ask rather than guess when an input is missing.

## When not to use it
- Do not compare values with incompatible units, populations, or assumptions.

## How to build it
Put the input in a JSON file and run:

```bash
node scripts/render.mjs okr-progress <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/okr-progress/source/template.tsx
- https://design-vault.pages.dev/designs/okr-progress/source/schema.ts
- https://design-vault.pages.dev/designs/okr-progress/source/sample-data.json
- https://design-vault.pages.dev/designs/okr-progress/sample.html

Files

Template, input schema, sample data and metadata. This document was generated from those four. The generated document

template.tsx

6.9 KB

/designs/okr-progress/source/template.tsx

Show contents
import type { ReactNode } from "react";
import type { Props } from "./schema";


const fmt = (n: number) => Number.isInteger(n) ? n.toLocaleString("ja-JP") : n.toLocaleString("ja-JP", {maximumFractionDigits: 2});
function Notes({children}:{children:ReactNode}) { return <details><summary>データ・判断条件を確認</summary>{children}</details>; }

const CSS = String.raw`
.asset{font-family:var(--font-sans);color:var(--color-primary);background:var(--color-surface);max-width:var(--container-slide);margin:auto;padding:3rem 2rem;line-height:1.6;font-size:var(--text-body-sm)}
.asset *{box-sizing:border-box}.asset header{margin-bottom:2rem}.asset .eyebrow{font-family:var(--font-mono);font-size:var(--text-label);letter-spacing:.08em;color:var(--color-secondary);margin-bottom:.75rem}.asset h1{font-size:var(--text-h1);font-weight:700;line-height:1.35;letter-spacing:-.025em;max-width:32em}.asset h2{font-size:var(--text-h3);font-weight:600;margin-bottom:.75rem}.asset h3{font-size:var(--text-body-md);font-weight:600}.asset .subtitle,.asset .muted{color:var(--color-secondary)}.asset .subtitle{margin-top:.75rem}.asset .number{font-variant-numeric:tabular-nums;font-family:var(--font-mono)}.asset .hero-number{font-size:var(--text-display);line-height:1.2;font-weight:700;letter-spacing:-.04em}.asset .callout{border-left:.25rem solid var(--color-tertiary);padding:1rem 1.5rem;background:var(--color-surface-subtle);margin-bottom:1.5rem}.asset .grid2{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem}.asset .grid3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1.5rem}.asset .panel{padding:1.5rem;background:var(--color-surface-subtle);border-radius:var(--radius-md);min-width:0}.asset .rule{border-top:1px solid var(--color-outline);padding-top:1rem;margin-top:1rem}.asset .pill{display:inline-block;padding:.25rem .75rem;border:1px solid var(--color-outline);border-radius:var(--radius-full);font-size:var(--text-label)}.asset .focus{background:var(--color-tertiary-container);color:var(--color-on-tertiary-container)}.asset .dark{background:var(--color-primary);color:var(--color-on-primary)}.asset .dark .muted{color:var(--color-on-primary-container)}.asset .row{display:grid;grid-template-columns:12rem minmax(0,1fr) 6rem;gap:1rem;align-items:center;padding:.75rem 0;border-bottom:1px solid var(--color-outline)}.asset .row>*{min-width:0}.asset .track{height:1rem;position:relative;background:var(--color-neutral);border-radius:var(--radius-sm)}.asset .bar{height:100%;background:var(--color-tertiary);border-radius:var(--radius-sm)}.asset .track.tall{height:2rem}.asset .flex{display:flex;gap:1rem;align-items:center;justify-content:space-between;flex-wrap:wrap}.asset .stack>*+*{margin-top:1rem}.asset ul{list-style:disc;padding-left:1.25rem}.asset li+li{margin-top:.5rem}.asset .plot{display:block;width:100%;height:auto;overflow:visible}.asset .plot text{font-family:var(--font-sans);font-size:14px;fill:var(--color-secondary)}.asset .plot .ink{fill:var(--color-primary)}.asset .plot .axis{stroke:var(--color-outline);stroke-width:1}.asset .plot .accent{stroke:var(--color-tertiary);fill:none;stroke-width:3}.asset .legend{display:flex;gap:1.5rem;flex-wrap:wrap;font-size:var(--text-label);margin-bottom:1rem}.asset .legend b{display:inline-block;width:.75rem;height:.75rem;margin-right:.5rem}.asset .ledger{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums}.asset .ledger caption{text-align:left;font-weight:600;margin:1rem 0}.asset .ledger th{font-weight:600;color:var(--color-secondary);text-align:left}.asset .ledger th,.asset .ledger td{padding:.75rem 1rem;border-bottom:1px solid var(--color-outline);vertical-align:top;overflow-wrap:anywhere}.asset .ledger th:first-child,.asset .ledger td:first-child{padding-left:0}.asset .ledger th:last-child,.asset .ledger td:last-child{padding-right:0}.asset .ledger tbody tr:hover{background:var(--color-surface-subtle)}.asset details{border-top:1px solid var(--color-outline);margin-top:1.5rem;padding-top:1rem}.asset summary{cursor:pointer;color:var(--color-secondary);padding:.5rem 0;min-height:2.75rem}.asset summary:focus-visible{outline:2px solid var(--color-tertiary);outline-offset:.25rem}.asset .small{font-size:var(--text-label)}.asset .mobile-only{display:none}.asset .horizontal-list{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem}.asset .board-column{border-top:.25rem solid var(--color-primary);padding-top:1rem;min-width:0}.asset .board-item{padding:1rem 0;border-bottom:1px solid var(--color-outline)}.asset .board-item p{margin-top:.5rem}.asset .statement{font-size:var(--text-h2);line-height:1.5;font-weight:600}.asset .meta-line{margin-top:.75rem;font-size:var(--text-label);color:var(--color-secondary)}.asset pre{white-space:pre-wrap;overflow-wrap:anywhere}.asset footer{margin-top:2rem;font-size:var(--text-label);color:var(--color-secondary)}
@media(max-width:640px){.asset{padding:1.5rem 1rem}.asset h1{font-size:var(--text-h2)}.asset header{margin-bottom:1.5rem}.asset .grid2,.asset .grid3,.asset .horizontal-list{grid-template-columns:1fr}.asset .row{grid-template-columns:minmax(0,1fr) 5rem;gap:.5rem}.asset .row>:first-child{grid-column:1/-1}.asset .desktop-only{display:none!important}.asset .mobile-only{display:block}.asset .panel{padding:1rem}.asset .ledger,.asset .ledger tbody,.asset .ledger tr,.asset .ledger td{display:block}.asset .ledger thead{display:none}.asset .ledger tr{padding:.75rem 0;border-bottom:1px solid var(--color-outline)}.asset .ledger td{display:grid;grid-template-columns:7rem minmax(0,1fr);gap:.75rem;border:0;padding:.25rem 0!important}.asset .ledger td:before{content:attr(data-label);color:var(--color-secondary);font-size:var(--text-label)}.asset .hero-number{font-size:var(--text-h1)}}
@media print{.asset{max-width:none;padding:1rem}.asset details>*{display:block}.asset summary{display:none}}
`;

export default function Template(d: Props) {
const progress=(r:Props["objectives"][number]["results"][number])=>(r.current-r.start)/(r.target-r.start)*100;
return <section className="asset asset-okr-progress"><style>{CSS}</style><header><p className="eyebrow">OKR PROGRESS</p><h1>{d.title}</h1>{d.subtitle && <p className="subtitle">{d.subtitle}</p>}</header>
<div className="stack">{d.objectives.map((o,i)=><article key={i}><div className="flex"><h2>{o.label}</h2><span className="pill">{o.owner}</span></div>{o.results.map((r,j)=><div className="row" key={j}><strong>{r.label}</strong><div><div className="track"><div className="bar" style={{width:Math.max(0,Math.min(100,progress(r)))+"%"}}/></div><p className="meta-line">開始 {r.start} → 現在 {r.current} → 目標 {r.target} {r.unit}</p></div><strong className="number">{fmt(progress(r))}%</strong></div>)}</article>)}</div><Notes><p>進捗=(現在−開始)÷(目標−開始)。表示帯は0〜100%に収めますが、実際の進捗値は切り捨てず表示します。</p></Notes>
<footer>複数指標の進捗を根拠なく平均して目標全体の達成率にしない。</footer></section>;
}

schema.ts

798 B

/designs/okr-progress/source/schema.ts

Show contents
import { z } from "zod";
const text = z.string().trim().min(1).max(160);
const short = z.string().trim().min(1).max(48);
const value = z.number().finite();
const positive = value.positive();

export const schema = z.object({
  title: text.describe("結論を伝える見出し"),
  subtitle: text.optional().describe("出典・時点・母集団や仮定"),
  objectives:z.array(z.object({label:short,owner:short,results:z.array(z.object({label:short,start:value,current:value,target:value,unit:short}).strict()).min(2).max(4)}).strict()).min(1).max(3).describe("目標と2〜4件の成果指標。開始と目標は異なる値"),
}).strict().refine(d=>d.objectives.every(o=>o.results.every(r=>r.start!==r.target)),"開始値と目標値は異なる");
export type Props = z.infer<typeof schema>;

meta.yaml

1.3 KB

/designs/okr-progress/source/meta.yaml

Show contents
id: okr-progress
name:
  ja: 目標と成果指標の進捗
  en: OKR Progress
purpose:
  ja: 目標ごとの成果指標について開始値・現在値・目標値から進捗を読む。
  en: OKR Progress with explicit source values and reading conditions.
status: published
still: ./still.png
category: decision
catalogOrder: 450
jobs:
  - measure-change
useCases:
  - kpi
layoutKinds:
  - stack
tags:
  - okr-progress
  - decision
  - offline
  - responsive
whenToUse:
  - ja: 目標ごとの成果指標について開始値・現在値・目標値から進捗を読む。
    en: Use to communicate okr progress.
avoidWhen:
  - ja: 同じ条件・単位で比較できないデータには使わない
    en: Do not compare values with incompatible units, populations, or assumptions.
requiredInputs:
  - ja: 複数指標の進捗を根拠なく平均して目標全体の達成率にしない。
    en: Provide the source values, labels and decision conditions described by the schema.
relatedLayouts:
  - kpi-cards
  - strategy-map
dataSchema:
  title: string — 結論を伝える見出し
  subtitle: string? — 出典・時点・母集団や仮定
  objectives: array (label, owner, results) — 目標と2〜4件の成果指標。開始と目標は異なる値
sources: []

sample-data.json

745 B

/designs/okr-progress/source/sample-data.json

Show contents
{
  "title": "成果指標の進捗と、次の課題を共有する",
  "subtitle": "架空サンプル|レイアウトの使用例",
  "objectives": [
    {
      "label": "手続きの待ち時間を減らす",
      "owner": "業務改革室",
      "results": [
        {
          "label": "平均所要日数",
          "start": 10,
          "current": 7,
          "target": 5,
          "unit": "日"
        },
        {
          "label": "電子申請率",
          "start": 40,
          "current": 76,
          "target": 85,
          "unit": "%"
        },
        {
          "label": "再申請率",
          "start": 12,
          "current": 8,
          "target": 4,
          "unit": "%"
        }
      ]
    }
  ]
}