@seanmars/tospec 0.23.0 → 0.26.0-beta
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/CHANGELOG.md +281 -0
- package/README.md +27 -9
- package/assets/claude-mod/tospec-dashboard/.claude-plugin/plugin.json +7 -0
- package/assets/claude-mod/tospec-dashboard/hooks/hooks.json +1 -0
- package/assets/claude-mod/tospec-dashboard/hooks/register.ts +695 -0
- package/assets/claude-mod/tospec-dashboard/tests/register.test.ts +448 -0
- package/assets/claude-mod/tospec-dashboard/types/index.d.ts +8 -0
- package/assets/dashboard/app.js +218 -1180
- package/assets/dashboard/index.html +31 -22
- package/assets/dashboard/lib/charts.js +355 -0
- package/assets/dashboard/lib/derive.js +375 -0
- package/assets/dashboard/lib/dom.js +121 -0
- package/assets/dashboard/lib/routes.js +52 -0
- package/assets/dashboard/lib/store.js +326 -0
- package/assets/dashboard/style.css +3103 -664
- package/assets/dashboard/views/alerts.js +170 -0
- package/assets/dashboard/views/change.js +234 -0
- package/assets/dashboard/views/docs.js +214 -0
- package/assets/dashboard/views/home.js +357 -0
- package/assets/dashboard/views/project.js +168 -0
- package/assets/dashboard/views/session.js +270 -0
- package/assets/dashboard/views/sidebar.js +131 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +116 -102
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +213 -100
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/dashboard-relay.d.ts +11 -0
- package/dist/commands/dashboard-relay.d.ts.map +1 -0
- package/dist/commands/dashboard-relay.js +88 -0
- package/dist/commands/dashboard-relay.js.map +1 -0
- package/dist/commands/dashboard.d.ts +69 -100
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +420 -598
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/decision.d.ts +38 -1
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +303 -27
- package/dist/commands/decision.js.map +1 -1
- package/dist/commands/metrics.d.ts +3 -4
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +3 -5
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/shared-output.d.ts +24 -0
- package/dist/commands/shared-output.d.ts.map +1 -1
- package/dist/commands/shared-output.js +21 -2
- package/dist/commands/shared-output.js.map +1 -1
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +12 -9
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/validate.d.ts +19 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +243 -64
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/workflow/instructions.d.ts +7 -1
- package/dist/commands/workflow/instructions.d.ts.map +1 -1
- package/dist/commands/workflow/instructions.js +299 -81
- package/dist/commands/workflow/instructions.js.map +1 -1
- package/dist/commands/workflow/new-change.d.ts.map +1 -1
- package/dist/commands/workflow/new-change.js +26 -17
- package/dist/commands/workflow/new-change.js.map +1 -1
- package/dist/commands/workflow/prompt-escape.d.ts +25 -0
- package/dist/commands/workflow/prompt-escape.d.ts.map +1 -0
- package/dist/commands/workflow/prompt-escape.js +62 -0
- package/dist/commands/workflow/prompt-escape.js.map +1 -0
- package/dist/commands/workflow/schemas.d.ts.map +1 -1
- package/dist/commands/workflow/schemas.js +34 -16
- package/dist/commands/workflow/schemas.js.map +1 -1
- package/dist/commands/workflow/shared.d.ts +46 -10
- package/dist/commands/workflow/shared.d.ts.map +1 -1
- package/dist/commands/workflow/shared.js +66 -30
- package/dist/commands/workflow/shared.js.map +1 -1
- package/dist/commands/workflow/status.d.ts +5 -2
- package/dist/commands/workflow/status.d.ts.map +1 -1
- package/dist/commands/workflow/status.js +62 -28
- package/dist/commands/workflow/status.js.map +1 -1
- package/dist/core/archive.d.ts +38 -0
- package/dist/core/archive.d.ts.map +1 -1
- package/dist/core/archive.js +492 -75
- package/dist/core/archive.js.map +1 -1
- package/dist/core/artifact-graph/index.d.ts +2 -2
- package/dist/core/artifact-graph/index.d.ts.map +1 -1
- package/dist/core/artifact-graph/index.js +2 -2
- package/dist/core/artifact-graph/index.js.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +8 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.js +58 -36
- package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
- package/dist/core/artifact-graph/resolver.d.ts +39 -12
- package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
- package/dist/core/artifact-graph/resolver.js +82 -36
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/artifact-graph/schema.d.ts +24 -0
- package/dist/core/artifact-graph/schema.d.ts.map +1 -1
- package/dist/core/artifact-graph/schema.js +32 -1
- package/dist/core/artifact-graph/schema.js.map +1 -1
- package/dist/core/artifact-graph/stub-detection.d.ts +10 -2
- package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -1
- package/dist/core/artifact-graph/stub-detection.js +18 -11
- package/dist/core/artifact-graph/stub-detection.js.map +1 -1
- package/dist/core/artifact-graph/types.d.ts +2 -2
- package/dist/core/artifact-graph/types.d.ts.map +1 -1
- package/dist/core/artifact-graph/types.js +10 -1
- package/dist/core/artifact-graph/types.js.map +1 -1
- package/dist/core/change-metadata/schema.d.ts.map +1 -1
- package/dist/core/change-metadata/schema.js +18 -4
- package/dist/core/change-metadata/schema.js.map +1 -1
- package/dist/core/change-presenter.d.ts.map +1 -1
- package/dist/core/change-presenter.js +13 -8
- package/dist/core/change-presenter.js.map +1 -1
- package/dist/core/change-status-policy.d.ts.map +1 -1
- package/dist/core/change-status-policy.js +3 -1
- package/dist/core/change-status-policy.js.map +1 -1
- package/dist/core/codex-metrics.d.ts +1 -1
- package/dist/core/codex-metrics.js +1 -1
- package/dist/core/config-schema.d.ts +8 -0
- package/dist/core/config-schema.d.ts.map +1 -1
- package/dist/core/config-schema.js +10 -0
- package/dist/core/config-schema.js.map +1 -1
- package/dist/core/dashboard-activity.d.ts +4 -4
- package/dist/core/dashboard-activity.js +4 -4
- package/dist/core/dashboard-data.d.ts +11 -5
- package/dist/core/dashboard-data.d.ts.map +1 -1
- package/dist/core/dashboard-data.js +54 -28
- package/dist/core/dashboard-data.js.map +1 -1
- package/dist/core/dashboard-hub.d.ts +258 -0
- package/dist/core/dashboard-hub.d.ts.map +1 -0
- package/dist/core/dashboard-hub.js +655 -0
- package/dist/core/dashboard-hub.js.map +1 -0
- package/dist/core/dashboard-routes.d.ts +57 -0
- package/dist/core/dashboard-routes.d.ts.map +1 -0
- package/dist/core/dashboard-routes.js +398 -0
- package/dist/core/dashboard-routes.js.map +1 -0
- package/dist/core/global-config.d.ts +71 -2
- package/dist/core/global-config.d.ts.map +1 -1
- package/dist/core/global-config.js +149 -4
- package/dist/core/global-config.js.map +1 -1
- package/dist/core/id.d.ts +9 -0
- package/dist/core/id.d.ts.map +1 -1
- package/dist/core/id.js +37 -0
- package/dist/core/id.js.map +1 -1
- package/dist/core/init.d.ts +9 -2
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +119 -33
- package/dist/core/init.js.map +1 -1
- package/dist/core/list.d.ts.map +1 -1
- package/dist/core/list.js +69 -45
- package/dist/core/list.js.map +1 -1
- package/dist/core/local-server.d.ts +10 -3
- package/dist/core/local-server.d.ts.map +1 -1
- package/dist/core/local-server.js +25 -5
- package/dist/core/local-server.js.map +1 -1
- package/dist/core/markdown-render.d.ts.map +1 -1
- package/dist/core/markdown-render.js +7 -2
- package/dist/core/markdown-render.js.map +1 -1
- package/dist/core/migrate.d.ts +24 -2
- package/dist/core/migrate.d.ts.map +1 -1
- package/dist/core/migrate.js +335 -59
- package/dist/core/migrate.js.map +1 -1
- package/dist/core/parsers/change-parser.d.ts +20 -1
- package/dist/core/parsers/change-parser.d.ts.map +1 -1
- package/dist/core/parsers/change-parser.js +76 -73
- package/dist/core/parsers/change-parser.js.map +1 -1
- package/dist/core/parsers/markdown-parser.d.ts +24 -0
- package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
- package/dist/core/parsers/markdown-parser.js +65 -10
- package/dist/core/parsers/markdown-parser.js.map +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +127 -6
- package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
- package/dist/core/parsers/requirement-blocks.js +349 -56
- package/dist/core/parsers/requirement-blocks.js.map +1 -1
- package/dist/core/parsers/requirement-text.d.ts +25 -1
- package/dist/core/parsers/requirement-text.d.ts.map +1 -1
- package/dist/core/parsers/requirement-text.js +62 -6
- package/dist/core/parsers/requirement-text.js.map +1 -1
- package/dist/core/parsers/spec-structure.d.ts.map +1 -1
- package/dist/core/parsers/spec-structure.js +36 -24
- package/dist/core/parsers/spec-structure.js.map +1 -1
- package/dist/core/project-config.d.ts +9 -2
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +30 -7
- package/dist/core/project-config.js.map +1 -1
- package/dist/core/project-layout.d.ts +26 -0
- package/dist/core/project-layout.d.ts.map +1 -1
- package/dist/core/project-layout.js +51 -1
- package/dist/core/project-layout.js.map +1 -1
- package/dist/core/schema-names.d.ts +17 -0
- package/dist/core/schema-names.d.ts.map +1 -1
- package/dist/core/schema-names.js +24 -0
- package/dist/core/schema-names.js.map +1 -1
- package/dist/core/shared/dashboard-mod.d.ts +24 -0
- package/dist/core/shared/dashboard-mod.d.ts.map +1 -0
- package/dist/core/shared/dashboard-mod.js +44 -0
- package/dist/core/shared/dashboard-mod.js.map +1 -0
- package/dist/core/shared/index.d.ts +2 -1
- package/dist/core/shared/index.d.ts.map +1 -1
- package/dist/core/shared/index.js +2 -1
- package/dist/core/shared/index.js.map +1 -1
- package/dist/core/shared/rules-generation.d.ts +12 -0
- package/dist/core/shared/rules-generation.d.ts.map +1 -1
- package/dist/core/shared/rules-generation.js +26 -2
- package/dist/core/shared/rules-generation.js.map +1 -1
- package/dist/core/shared/skill-generation.d.ts +7 -0
- package/dist/core/shared/skill-generation.d.ts.map +1 -1
- package/dist/core/shared/skill-generation.js +19 -0
- package/dist/core/shared/skill-generation.js.map +1 -1
- package/dist/core/shared/tool-detection.d.ts +10 -1
- package/dist/core/shared/tool-detection.d.ts.map +1 -1
- package/dist/core/shared/tool-detection.js +37 -6
- package/dist/core/shared/tool-detection.js.map +1 -1
- package/dist/core/spec-presenter.d.ts.map +1 -1
- package/dist/core/spec-presenter.js +23 -1
- package/dist/core/spec-presenter.js.map +1 -1
- package/dist/core/specs-apply.d.ts +98 -1
- package/dist/core/specs-apply.d.ts.map +1 -1
- package/dist/core/specs-apply.js +426 -63
- package/dist/core/specs-apply.js.map +1 -1
- package/dist/core/templates/fragments/change-diff.d.ts +37 -0
- package/dist/core/templates/fragments/change-diff.d.ts.map +1 -0
- package/dist/core/templates/fragments/change-diff.js +37 -0
- package/dist/core/templates/fragments/change-diff.js.map +1 -0
- package/dist/core/templates/fragments/sync-fail.d.ts +8 -0
- package/dist/core/templates/fragments/sync-fail.d.ts.map +1 -0
- package/dist/core/templates/fragments/sync-fail.js +8 -0
- package/dist/core/templates/fragments/sync-fail.js.map +1 -0
- package/dist/core/templates/fragments/validate.d.ts +14 -1
- package/dist/core/templates/fragments/validate.d.ts.map +1 -1
- package/dist/core/templates/fragments/validate.js +14 -1
- package/dist/core/templates/fragments/validate.js.map +1 -1
- package/dist/core/templates/fragments/verify.d.ts +2 -2
- package/dist/core/templates/fragments/verify.d.ts.map +1 -1
- package/dist/core/templates/fragments/verify.js +8 -5
- package/dist/core/templates/fragments/verify.js.map +1 -1
- package/dist/core/templates/workflows/apply.d.ts +1 -1
- package/dist/core/templates/workflows/apply.d.ts.map +1 -1
- package/dist/core/templates/workflows/apply.js +29 -10
- package/dist/core/templates/workflows/apply.js.map +1 -1
- package/dist/core/templates/workflows/archive.d.ts.map +1 -1
- package/dist/core/templates/workflows/archive.js +27 -9
- package/dist/core/templates/workflows/archive.js.map +1 -1
- package/dist/core/templates/workflows/decision.d.ts.map +1 -1
- package/dist/core/templates/workflows/decision.js +10 -4
- package/dist/core/templates/workflows/decision.js.map +1 -1
- package/dist/core/templates/workflows/explore.d.ts +2 -2
- package/dist/core/templates/workflows/explore.d.ts.map +1 -1
- package/dist/core/templates/workflows/explore.js +7 -3
- package/dist/core/templates/workflows/explore.js.map +1 -1
- package/dist/core/templates/workflows/grill.d.ts +2 -2
- package/dist/core/templates/workflows/grill.d.ts.map +1 -1
- package/dist/core/templates/workflows/grill.js +5 -4
- package/dist/core/templates/workflows/grill.js.map +1 -1
- package/dist/core/templates/workflows/issue.d.ts.map +1 -1
- package/dist/core/templates/workflows/issue.js +9 -3
- package/dist/core/templates/workflows/issue.js.map +1 -1
- package/dist/core/templates/workflows/propose.d.ts.map +1 -1
- package/dist/core/templates/workflows/propose.js +4 -1
- package/dist/core/templates/workflows/propose.js.map +1 -1
- package/dist/core/templates/workflows/sync.d.ts.map +1 -1
- package/dist/core/templates/workflows/sync.js +25 -11
- package/dist/core/templates/workflows/sync.js.map +1 -1
- package/dist/core/templates/workflows/update.d.ts.map +1 -1
- package/dist/core/templates/workflows/update.js +17 -10
- package/dist/core/templates/workflows/update.js.map +1 -1
- package/dist/core/update.d.ts +5 -0
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +69 -29
- package/dist/core/update.js.map +1 -1
- package/dist/core/validation/constants.d.ts +7 -1
- package/dist/core/validation/constants.d.ts.map +1 -1
- package/dist/core/validation/constants.js +16 -2
- package/dist/core/validation/constants.js.map +1 -1
- package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
- package/dist/core/validation/purpose-placeholder.js +35 -11
- package/dist/core/validation/purpose-placeholder.js.map +1 -1
- package/dist/core/validation/section-validator.d.ts +7 -0
- package/dist/core/validation/section-validator.d.ts.map +1 -1
- package/dist/core/validation/section-validator.js +22 -2
- package/dist/core/validation/section-validator.js.map +1 -1
- package/dist/core/validation/sister-changes.d.ts +93 -0
- package/dist/core/validation/sister-changes.d.ts.map +1 -0
- package/dist/core/validation/sister-changes.js +180 -0
- package/dist/core/validation/sister-changes.js.map +1 -0
- package/dist/core/validation/task-markers.d.ts +9 -0
- package/dist/core/validation/task-markers.d.ts.map +1 -0
- package/dist/core/validation/task-markers.js +71 -0
- package/dist/core/validation/task-markers.js.map +1 -0
- package/dist/core/validation/types.d.ts +58 -0
- package/dist/core/validation/types.d.ts.map +1 -1
- package/dist/core/validation/types.js +22 -0
- package/dist/core/validation/types.js.map +1 -1
- package/dist/core/validation/validator.d.ts +79 -2
- package/dist/core/validation/validator.d.ts.map +1 -1
- package/dist/core/validation/validator.js +565 -84
- package/dist/core/validation/validator.js.map +1 -1
- package/dist/utils/change-metadata.d.ts +62 -5
- package/dist/utils/change-metadata.d.ts.map +1 -1
- package/dist/utils/change-metadata.js +193 -31
- package/dist/utils/change-metadata.js.map +1 -1
- package/dist/utils/change-utils.d.ts +21 -0
- package/dist/utils/change-utils.d.ts.map +1 -1
- package/dist/utils/change-utils.js +168 -53
- package/dist/utils/change-utils.js.map +1 -1
- package/dist/utils/command-line.d.ts +11 -0
- package/dist/utils/command-line.d.ts.map +1 -0
- package/dist/utils/command-line.js +43 -0
- package/dist/utils/command-line.js.map +1 -0
- package/dist/utils/display-width.d.ts +10 -0
- package/dist/utils/display-width.d.ts.map +1 -0
- package/dist/utils/display-width.js +15 -0
- package/dist/utils/display-width.js.map +1 -0
- package/dist/utils/file-system.d.ts +16 -0
- package/dist/utils/file-system.d.ts.map +1 -1
- package/dist/utils/file-system.js +49 -2
- package/dist/utils/file-system.js.map +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +11 -5
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/interactive.d.ts.map +1 -1
- package/dist/utils/interactive.js +3 -1
- package/dist/utils/interactive.js.map +1 -1
- package/dist/utils/item-discovery.d.ts +3 -2
- package/dist/utils/item-discovery.d.ts.map +1 -1
- package/dist/utils/item-discovery.js +34 -16
- package/dist/utils/item-discovery.js.map +1 -1
- package/dist/utils/link.d.ts +0 -1
- package/dist/utils/link.d.ts.map +1 -1
- package/dist/utils/link.js +0 -1
- package/dist/utils/link.js.map +1 -1
- package/dist/utils/match.d.ts.map +1 -1
- package/dist/utils/match.js +10 -1
- package/dist/utils/match.js.map +1 -1
- package/dist/utils/option-flags.d.ts +9 -0
- package/dist/utils/option-flags.d.ts.map +1 -0
- package/dist/utils/option-flags.js +12 -0
- package/dist/utils/option-flags.js.map +1 -0
- package/dist/utils/sentence.d.ts +9 -0
- package/dist/utils/sentence.d.ts.map +1 -0
- package/dist/utils/sentence.js +12 -0
- package/dist/utils/sentence.js.map +1 -0
- package/dist/utils/spec-files.d.ts +84 -1
- package/dist/utils/spec-files.d.ts.map +1 -1
- package/dist/utils/spec-files.js +192 -3
- package/dist/utils/spec-files.js.map +1 -1
- package/dist/utils/task-progress.d.ts.map +1 -1
- package/dist/utils/task-progress.js +32 -9
- package/dist/utils/task-progress.js.map +1 -1
- package/dist/utils/timestamp.d.ts +27 -0
- package/dist/utils/timestamp.d.ts.map +1 -1
- package/dist/utils/timestamp.js +37 -0
- package/dist/utils/timestamp.js.map +1 -1
- package/package.json +6 -2
- package/schemas/decision/templates/decision.md +2 -2
- package/schemas/issue/schema.yaml +12 -6
- package/schemas/issue/templates/task.md +1 -1
- package/schemas/sdd/schema.yaml +21 -10
- package/schemas/sdd/templates/proposal.md +1 -1
- package/assets/dashboard/chart.umd.js +0 -14
|
@@ -1,34 +1,43 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="zh-Hant-TW">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<
|
|
6
|
+
<meta name="color-scheme" content="light dark" />
|
|
7
|
+
<title>tospec</title>
|
|
8
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='7.5' fill='%23262b30'/%3E%3Ccircle cx='8' cy='8' r='3.6' fill='none' stroke='%23fff' stroke-width='2.4'/%3E%3C/svg%3E" />
|
|
7
9
|
<link rel="stylesheet" href="/assets/style.css" />
|
|
8
10
|
</head>
|
|
9
11
|
<body>
|
|
10
|
-
<header class="
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
12
|
+
<header class="signbar" id="signbar">
|
|
13
|
+
<button id="nav-toggle" type="button" class="sign-control sign-button nav-toggle" aria-controls="sidebar" aria-expanded="false" title="Projects"></button>
|
|
14
|
+
<a class="wordmark" href="#/" aria-label="tospec 總覽">tospec</a>
|
|
15
|
+
<nav class="crumbs-nav" aria-label="目前位置">
|
|
16
|
+
<ol id="crumbs" class="crumbs"></ol>
|
|
17
|
+
<a id="crumb-back" class="crumb-back" hidden></a>
|
|
18
|
+
</nav>
|
|
19
|
+
<div class="signbar-tools">
|
|
20
|
+
<span id="hub-status" class="hub-status" role="status"></span>
|
|
21
|
+
<label class="theme-field">
|
|
22
|
+
<span class="sr-only">主題</span>
|
|
23
|
+
<select id="theme-select" class="sign-control">
|
|
24
|
+
<option value="system">跟隨系統</option>
|
|
25
|
+
<option value="light">淺色</option>
|
|
26
|
+
<option value="dark">深色</option>
|
|
27
|
+
<option value="dracula">Dracula</option>
|
|
28
|
+
</select>
|
|
29
|
+
</label>
|
|
30
|
+
<button id="notify-btn" type="button" class="sign-control sign-button" aria-pressed="false"></button>
|
|
31
|
+
<button id="refresh-btn" type="button" class="sign-control sign-button" title="向連線中的 session 要求新快照">重新整理</button>
|
|
28
32
|
</div>
|
|
29
33
|
</header>
|
|
30
|
-
<
|
|
31
|
-
|
|
34
|
+
<div class="shell">
|
|
35
|
+
<aside id="sidebar" class="sidebar" aria-label="Projects"></aside>
|
|
36
|
+
<main id="view" class="view" tabindex="-1"></main>
|
|
37
|
+
</div>
|
|
38
|
+
<div id="nav-scrim" class="nav-scrim" hidden></div>
|
|
39
|
+
<aside id="session-panel" class="session-panel" hidden></aside>
|
|
40
|
+
<div id="toasts" class="toasts" aria-live="polite"></div>
|
|
32
41
|
<script type="module" src="/assets/app.js"></script>
|
|
33
42
|
</body>
|
|
34
43
|
</html>
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// The project page's four charts, drawn as SVG at the container's real pixel
|
|
2
|
+
// width (redrawn on resize) so text stays at its set size instead of scaling
|
|
3
|
+
// with a viewBox. Colours are CSS classes reading theme variables, so a theme
|
|
4
|
+
// switch restyles a drawn chart without redrawing it.
|
|
5
|
+
|
|
6
|
+
import { el, svg, icon } from './dom.js';
|
|
7
|
+
import { parseLooseDate, formatDate, weeklyArchiveCounts, relTime } from './derive.js';
|
|
8
|
+
import { routes } from './routes.js';
|
|
9
|
+
|
|
10
|
+
const DAY_MS = 86400000;
|
|
11
|
+
const observers = new Set();
|
|
12
|
+
|
|
13
|
+
/** Called before every route render: charts from the previous view stop observing. */
|
|
14
|
+
export function releaseCharts() {
|
|
15
|
+
for (const ro of observers) ro.disconnect();
|
|
16
|
+
observers.clear();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function figure({ title, summary, draw, table, legend }) {
|
|
20
|
+
const plot = el('div', { class: 'chart-plot' });
|
|
21
|
+
const tip = el('div', { class: 'chart-tip', role: 'status' });
|
|
22
|
+
tip.hidden = true;
|
|
23
|
+
const fig = el('figure', { class: 'chart' }, [
|
|
24
|
+
el('figcaption', { class: 'chart-caption' }, [el('h3', { class: 'chart-title', text: title }), summary ? el('p', { class: 'chart-summary', text: summary }) : null]),
|
|
25
|
+
plot,
|
|
26
|
+
legend || null,
|
|
27
|
+
tip,
|
|
28
|
+
table ? el('details', { class: 'chart-table' }, [el('summary', {}, [icon('chevron', 14), '以表格檢視']), table]) : null,
|
|
29
|
+
]);
|
|
30
|
+
if (draw) {
|
|
31
|
+
let lastWidth = 0;
|
|
32
|
+
const redraw = () => {
|
|
33
|
+
const width = Math.floor(plot.clientWidth);
|
|
34
|
+
if (!width || width === lastWidth) return;
|
|
35
|
+
lastWidth = width;
|
|
36
|
+
plot.replaceChildren(draw(width));
|
|
37
|
+
};
|
|
38
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
39
|
+
const ro = new ResizeObserver(() => redraw());
|
|
40
|
+
ro.observe(plot);
|
|
41
|
+
observers.add(ro);
|
|
42
|
+
}
|
|
43
|
+
wireTooltip(fig, plot, tip);
|
|
44
|
+
}
|
|
45
|
+
return fig;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Marks carry `data-tip`; one tooltip per chart follows the pointer. */
|
|
49
|
+
function wireTooltip(fig, plot, tip) {
|
|
50
|
+
plot.addEventListener('pointermove', (event) => {
|
|
51
|
+
const mark = event.target.closest && event.target.closest('[data-tip]');
|
|
52
|
+
if (!mark) {
|
|
53
|
+
tip.hidden = true;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
tip.textContent = mark.getAttribute('data-tip');
|
|
57
|
+
tip.hidden = false;
|
|
58
|
+
const box = fig.getBoundingClientRect();
|
|
59
|
+
const x = event.clientX - box.left;
|
|
60
|
+
const flip = x > box.width - 220;
|
|
61
|
+
tip.style.setProperty('left', (flip ? x - 12 : x + 12) + 'px');
|
|
62
|
+
tip.style.setProperty('top', event.clientY - box.top + 12 + 'px');
|
|
63
|
+
tip.classList.toggle('is-flipped', flip);
|
|
64
|
+
});
|
|
65
|
+
plot.addEventListener('pointerleave', () => {
|
|
66
|
+
tip.hidden = true;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function dataTable(headers, rows) {
|
|
71
|
+
return el('table', { class: 'data-table' }, [
|
|
72
|
+
el('thead', {}, [el('tr', {}, headers.map((h) => el('th', { scope: 'col', text: h })))]),
|
|
73
|
+
el('tbody', {}, rows.map((r) => el('tr', {}, r.map((c) => el('td', { text: String(c) }))))),
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function emptyFigure(title, text) {
|
|
78
|
+
return el('figure', { class: 'chart chart-empty' }, [
|
|
79
|
+
el('figcaption', { class: 'chart-caption' }, [el('h3', { class: 'chart-title', text: title })]),
|
|
80
|
+
el('p', { class: 'note', text }),
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Truncates an ASCII-ish label to roughly `px` at 12px; the full text rides in the tooltip and table. */
|
|
85
|
+
function fit(text, px) {
|
|
86
|
+
const max = Math.max(4, Math.floor(px / 6.8));
|
|
87
|
+
return text.length > max ? text.slice(0, max - 1) + '…' : text;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Column/bar with a 4px rounded data end and a square baseline end. */
|
|
91
|
+
function barPath(x, y, w, h, horizontal) {
|
|
92
|
+
const r = Math.min(4, horizontal ? w / 2 : h / 2, horizontal ? h / 2 : w / 2);
|
|
93
|
+
if (horizontal) {
|
|
94
|
+
return `M${x},${y}H${x + w - r}Q${x + w},${y} ${x + w},${y + r}V${y + h - r}Q${x + w},${y + h} ${x + w - r},${y + h}H${x}Z`;
|
|
95
|
+
}
|
|
96
|
+
return `M${x},${y + h}V${y + r}Q${x},${y} ${x + r},${y}H${x + w - r}Q${x + w},${y} ${x + w},${y + r}V${y + h}Z`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function niceMax(value) {
|
|
100
|
+
if (value <= 4) return Math.max(1, value);
|
|
101
|
+
const step = Math.pow(10, Math.floor(Math.log10(value)));
|
|
102
|
+
return Math.ceil(value / step) * step;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const md = (d) => `${d.getMonth() + 1}/${d.getDate()}`;
|
|
106
|
+
|
|
107
|
+
// --- 1. in transit -------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Each active change's journey: departed (`created`) to last seen
|
|
111
|
+
* (`lastModified`) as a thick run in the line colour, then dashed idle time up
|
|
112
|
+
* to today. A last-seen dot left of the threshold line means the change has
|
|
113
|
+
* gone stale.
|
|
114
|
+
*/
|
|
115
|
+
export function transitChart(changes, staleDays, now) {
|
|
116
|
+
const title = 'change 存在天數';
|
|
117
|
+
if (!changes.length) return emptyFigure(title, '沒有進行中的 change.');
|
|
118
|
+
const rows = changes
|
|
119
|
+
.map((c) => ({ name: c.name, created: parseLooseDate(c.created), seen: parseLooseDate(c.lastModified) }))
|
|
120
|
+
.filter((r) => r.seen)
|
|
121
|
+
.sort((a, b) => a.seen - b.seen);
|
|
122
|
+
const stale = rows.filter((r) => now - r.seen.getTime() > staleDays * DAY_MS).length;
|
|
123
|
+
const summary = stale
|
|
124
|
+
? `${rows.length} 個進行中的 change, ${stale} 個超過 ${staleDays} 天沒有更新.`
|
|
125
|
+
: `${rows.length} 個進行中的 change, 都在 ${staleDays} 天內有更新.`;
|
|
126
|
+
const threshold = now - staleDays * DAY_MS;
|
|
127
|
+
let tMin = Math.min(threshold - 2 * DAY_MS, ...rows.map((r) => (r.created ? r.created.getTime() : r.seen.getTime())));
|
|
128
|
+
tMin -= (now - tMin) * 0.03;
|
|
129
|
+
|
|
130
|
+
const draw = (width) => {
|
|
131
|
+
const labelW = Math.min(180, Math.round(width * 0.32));
|
|
132
|
+
const x0 = labelW + 12;
|
|
133
|
+
const x1 = width - 12;
|
|
134
|
+
const top = 26;
|
|
135
|
+
const rowH = 30;
|
|
136
|
+
const height = top + rows.length * rowH + 26;
|
|
137
|
+
const x = (t) => x0 + ((t - tMin) / (now - tMin)) * (x1 - x0);
|
|
138
|
+
const g = svg('svg', { class: 'chart-svg', width, height, viewBox: `0 0 ${width} ${height}`, role: 'img', 'aria-label': summary });
|
|
139
|
+
|
|
140
|
+
// Axis first so marks sit over it.
|
|
141
|
+
const axisY = top + rows.length * rowH + 4;
|
|
142
|
+
g.append(svg('line', { class: 'axis', x1: x0, x2: x1, y1: axisY, y2: axisY }));
|
|
143
|
+
const ticks = 4;
|
|
144
|
+
for (let i = 0; i <= ticks; i++) {
|
|
145
|
+
const t = tMin + ((now - tMin) * i) / ticks;
|
|
146
|
+
const tx = x(t);
|
|
147
|
+
g.append(svg('line', { class: 'grid', x1: tx, x2: tx, y1: top - 6, y2: axisY }));
|
|
148
|
+
g.append(svg('text', { class: 'tick', x: tx, y: axisY + 16, 'text-anchor': i === 0 ? 'start' : i === ticks ? 'end' : 'middle', text: i === ticks ? '今天' : md(new Date(t)) }));
|
|
149
|
+
}
|
|
150
|
+
const tx = x(threshold);
|
|
151
|
+
g.append(svg('line', { class: 'threshold', x1: tx, x2: tx, y1: top - 10, y2: axisY }));
|
|
152
|
+
g.append(svg('text', { class: 'threshold-label', x: tx - 4, y: top - 14, 'text-anchor': 'end', text: `閒置門檻 ${staleDays} 天` }));
|
|
153
|
+
|
|
154
|
+
rows.forEach((r, i) => {
|
|
155
|
+
const y = top + i * rowH + rowH / 2;
|
|
156
|
+
const isStale = now - r.seen.getTime() > staleDays * DAY_MS;
|
|
157
|
+
const idle = Math.floor((now - r.seen.getTime()) / DAY_MS);
|
|
158
|
+
const age = r.created ? Math.floor((now - r.created.getTime()) / DAY_MS) : null;
|
|
159
|
+
const tipText = `${r.name} · ${age == null ? '未記錄 created' : '存在 ' + age + ' 天'} · 最後更新 ${relTime(r.seen, now)}`;
|
|
160
|
+
const row = svg('g', { class: 'transit-row' + (isStale ? ' is-stale' : ''), 'data-tip': tipText });
|
|
161
|
+
row.append(svg('rect', { class: 'hit', x: 0, y: y - rowH / 2, width, height: rowH }));
|
|
162
|
+
row.append(svg('text', { class: 'row-label', x: 0, y: y + 4, text: fit(r.name, labelW) }));
|
|
163
|
+
if (r.created && r.created <= r.seen) {
|
|
164
|
+
row.append(svg('line', { class: 'run', x1: x(r.created.getTime()), x2: x(r.seen.getTime()), y1: y, y2: y }));
|
|
165
|
+
}
|
|
166
|
+
row.append(svg('line', { class: 'idle', x1: x(r.seen.getTime()), x2: x1, y1: y, y2: y }));
|
|
167
|
+
row.append(svg('circle', { class: 'seen', cx: x(r.seen.getTime()), cy: y, r: 4.5 }));
|
|
168
|
+
g.append(row);
|
|
169
|
+
});
|
|
170
|
+
return g;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const table = dataTable(
|
|
174
|
+
['change', 'created', '最後更新', '閒置天數'],
|
|
175
|
+
rows.map((r) => [r.name, r.created ? formatDate(r.created) : '未記錄', formatDate(r.seen), Math.floor((now - r.seen.getTime()) / DAY_MS)])
|
|
176
|
+
);
|
|
177
|
+
return figure({ title, summary, draw, table });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- 2. weekly archives --------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
export function archiveRateChart(archivedChanges, now) {
|
|
183
|
+
const title = '每週歸檔數';
|
|
184
|
+
const buckets = weeklyArchiveCounts(archivedChanges, now, 12);
|
|
185
|
+
const total = buckets.reduce((n, b) => n + b.count, 0);
|
|
186
|
+
const peak = Math.max(...buckets.map((b) => b.count));
|
|
187
|
+
if (total === 0) return emptyFigure(title, '近 12 週沒有歸檔任何 change.');
|
|
188
|
+
const summary = `近 12 週歸檔 ${total} 個 change, 單週最多 ${peak} 個.`;
|
|
189
|
+
const top = niceMax(peak);
|
|
190
|
+
const latest = buckets.length - 1;
|
|
191
|
+
// Label sparingly: the first peak week and the current week, nothing else;
|
|
192
|
+
// the axis and the tooltip carry the rest.
|
|
193
|
+
const labelled = new Set([buckets.findIndex((b) => b.count === peak)]);
|
|
194
|
+
if (buckets[latest].count > 0) labelled.add(latest);
|
|
195
|
+
|
|
196
|
+
const draw = (width) => {
|
|
197
|
+
const x0 = 28;
|
|
198
|
+
const x1 = width - 8;
|
|
199
|
+
const y0 = 18;
|
|
200
|
+
const y1 = 150;
|
|
201
|
+
const height = y1 + 26;
|
|
202
|
+
const slot = (x1 - x0) / buckets.length;
|
|
203
|
+
const barW = Math.min(24, slot * 0.6);
|
|
204
|
+
const y = (v) => y1 - (v / top) * (y1 - y0);
|
|
205
|
+
const g = svg('svg', { class: 'chart-svg', width, height, viewBox: `0 0 ${width} ${height}`, role: 'img', 'aria-label': summary });
|
|
206
|
+
const tickValues = top >= 4 ? [0, top / 2, top] : [0, top];
|
|
207
|
+
for (const v of tickValues) {
|
|
208
|
+
g.append(svg('line', { class: v === 0 ? 'axis' : 'grid', x1: x0, x2: x1, y1: y(v), y2: y(v) }));
|
|
209
|
+
g.append(svg('text', { class: 'tick', x: x0 - 8, y: y(v) + 4, 'text-anchor': 'end', text: String(v) }));
|
|
210
|
+
}
|
|
211
|
+
buckets.forEach((b, i) => {
|
|
212
|
+
const cx = x0 + (i + 0.5) * slot;
|
|
213
|
+
const col = svg('g', { class: 'column', 'data-tip': `${md(b.start)} 起的那週: ${b.count} 個` });
|
|
214
|
+
col.append(svg('rect', { class: 'hit', x: cx - slot / 2, y: y0 - 10, width: slot, height: y1 - y0 + 10 }));
|
|
215
|
+
if (b.count > 0) {
|
|
216
|
+
col.append(svg('path', { class: 'bar', d: barPath(cx - barW / 2, y(b.count), barW, y1 - y(b.count), false) }));
|
|
217
|
+
if (labelled.has(i)) {
|
|
218
|
+
col.append(svg('text', { class: 'value', x: cx, y: y(b.count) - 6, 'text-anchor': 'middle', text: String(b.count) }));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (i % 3 === 0 || i === latest) {
|
|
222
|
+
col.append(svg('text', { class: 'tick', x: cx, y: y1 + 16, 'text-anchor': 'middle', text: i === latest ? '本週' : md(b.start) }));
|
|
223
|
+
}
|
|
224
|
+
g.append(col);
|
|
225
|
+
});
|
|
226
|
+
return g;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const table = dataTable(['週起始', '歸檔數'], buckets.map((b) => [formatDate(b.start), b.count]));
|
|
230
|
+
return figure({ title, summary, draw, table });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- 3. decision timeline ------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
const ADR_SHAPES = ['accepted', 'proposed', 'superseded'];
|
|
236
|
+
|
|
237
|
+
function adrMark(status, cx, cy) {
|
|
238
|
+
if (status === 'accepted') return svg('circle', { class: 'adr adr-accepted', cx, cy, r: 5 });
|
|
239
|
+
if (status === 'proposed') return svg('circle', { class: 'adr adr-proposed', cx, cy, r: 4.5 });
|
|
240
|
+
if (status === 'superseded') {
|
|
241
|
+
return svg('path', { class: 'adr adr-superseded', d: `M${cx - 3.5},${cy - 3.5}L${cx + 3.5},${cy + 3.5}M${cx + 3.5},${cy - 3.5}L${cx - 3.5},${cy + 3.5}` });
|
|
242
|
+
}
|
|
243
|
+
return svg('path', { class: 'adr adr-other', d: `M${cx},${cy - 5}L${cx + 5},${cy}L${cx},${cy + 5}L${cx - 5},${cy}Z` });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function legendSwatch(status) {
|
|
247
|
+
return svg('svg', { width: 14, height: 14, viewBox: '0 0 14 14', 'aria-hidden': 'true', class: 'legend-swatch' }, [adrMark(status, 7, 7)]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function decisionTimeline(key, decisions, now) {
|
|
251
|
+
const title = 'ADR 時間軸';
|
|
252
|
+
if (!decisions || !decisions.length) return emptyFigure(title, '這個 project 還沒有決策記錄.');
|
|
253
|
+
const items = decisions
|
|
254
|
+
.map((d) => ({ ...d, when: parseLooseDate(d.date) }))
|
|
255
|
+
.filter((d) => d.when)
|
|
256
|
+
.sort((a, b) => a.when - b.when);
|
|
257
|
+
const counts = {};
|
|
258
|
+
for (const d of decisions) counts[d.status] = (counts[d.status] || 0) + 1;
|
|
259
|
+
const open = counts.proposed || 0;
|
|
260
|
+
const summary = `${decisions.length} 筆決策` + (open ? `, ${open} 筆仍是 proposed.` : ', 沒有懸而未決的 proposed.');
|
|
261
|
+
let tMin = items.length ? items[0].when.getTime() : now - 30 * DAY_MS;
|
|
262
|
+
tMin = Math.min(tMin, now - 30 * DAY_MS);
|
|
263
|
+
tMin -= (now - tMin) * 0.03;
|
|
264
|
+
|
|
265
|
+
const draw = (width) => {
|
|
266
|
+
const x0 = 10;
|
|
267
|
+
const x1 = width - 10;
|
|
268
|
+
const x = (t) => x0 + ((t - tMin) / (now - tMin)) * (x1 - x0);
|
|
269
|
+
const levels = [];
|
|
270
|
+
const placed = items.map((d) => {
|
|
271
|
+
const px = x(d.when.getTime());
|
|
272
|
+
let level = 0;
|
|
273
|
+
while (levels.some((p) => p.level === level && Math.abs(p.x - px) < 11)) level++;
|
|
274
|
+
levels.push({ x: px, level });
|
|
275
|
+
return { d, px, level };
|
|
276
|
+
});
|
|
277
|
+
const maxLevel = Math.max(0, ...placed.map((p) => p.level));
|
|
278
|
+
const base = 18 + maxLevel * 13 + 8;
|
|
279
|
+
const height = base + 30;
|
|
280
|
+
const g = svg('svg', { class: 'chart-svg', width, height, viewBox: `0 0 ${width} ${height}`, role: 'img', 'aria-label': summary });
|
|
281
|
+
g.append(svg('line', { class: 'axis', x1: x0, x2: x1, y1: base + 8, y2: base + 8 }));
|
|
282
|
+
// Month starts as ticks, thinned to keep labels apart; evenly spaced
|
|
283
|
+
// ticks over a short range repeated the same month.
|
|
284
|
+
const todayX = x(now);
|
|
285
|
+
let lastX = -Infinity;
|
|
286
|
+
const month = new Date(tMin);
|
|
287
|
+
month.setDate(1);
|
|
288
|
+
month.setHours(0, 0, 0, 0);
|
|
289
|
+
for (month.setMonth(month.getMonth() + 1); month.getTime() < now; month.setMonth(month.getMonth() + 1)) {
|
|
290
|
+
const mx = x(month.getTime());
|
|
291
|
+
if (mx - lastX < 64 || todayX - mx < 52) continue;
|
|
292
|
+
g.append(svg('line', { class: 'grid', x1: mx, x2: mx, y1: base + 4, y2: base + 12 }));
|
|
293
|
+
g.append(svg('text', { class: 'tick', x: mx, y: base + 26, 'text-anchor': 'middle', text: formatDate(month).slice(0, 7) }));
|
|
294
|
+
lastX = mx;
|
|
295
|
+
}
|
|
296
|
+
g.append(svg('text', { class: 'tick', x: todayX, y: base + 26, 'text-anchor': 'end', text: '今天' }));
|
|
297
|
+
for (const { d, px, level } of placed) {
|
|
298
|
+
const cy = base - level * 13;
|
|
299
|
+
const link = svg('a', { href: routes.decisions(key, d.file), 'data-tip': `${formatDate(d.when)} · ${d.status} · ${d.title}`, 'aria-label': `${d.title} (${d.status})` }, [
|
|
300
|
+
svg('rect', { class: 'hit', x: px - 6, y: cy - 6, width: 12, height: 12 }),
|
|
301
|
+
adrMark(d.status, px, cy),
|
|
302
|
+
]);
|
|
303
|
+
g.append(link);
|
|
304
|
+
}
|
|
305
|
+
return g;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const statuses = [...ADR_SHAPES.filter((s) => counts[s]), ...Object.keys(counts).filter((s) => !ADR_SHAPES.includes(s))];
|
|
309
|
+
const legend = el('ul', { class: 'chart-legend' }, statuses.map((s) => el('li', {}, [legendSwatch(s), el('span', { text: `${s} ${counts[s]}` })])));
|
|
310
|
+
const table = dataTable(['日期', '標題', 'status'], items.slice().reverse().map((d) => [formatDate(d.when), d.title, d.status]));
|
|
311
|
+
return figure({ title, summary, draw, table, legend });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// --- 4. spec size ----------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
export function specSizeChart(key, specs) {
|
|
317
|
+
const title = 'Spec 規模';
|
|
318
|
+
if (!specs.length) return emptyFigure(title, '還沒有任何 spec. change 歸檔時, 它的 delta spec 會合併進 tospec/specs/.');
|
|
319
|
+
const sorted = specs.slice().sort((a, b) => b.requirementCount - a.requirementCount || a.id.localeCompare(b.id));
|
|
320
|
+
const total = sorted.reduce((n, s) => n + s.requirementCount, 0);
|
|
321
|
+
const summary = `${specs.length} 個 spec, 共 ${total} 條 requirement.`;
|
|
322
|
+
const LIMIT = 10;
|
|
323
|
+
const shown = sorted.slice(0, LIMIT);
|
|
324
|
+
const rest = sorted.slice(LIMIT);
|
|
325
|
+
const rows = shown.map((s) => ({ label: s.id, value: s.requirementCount, href: routes.spec(key, s.id) }));
|
|
326
|
+
if (rest.length) rows.push({ label: `其他 ${rest.length} 個 spec`, value: rest.reduce((n, s) => n + s.requirementCount, 0), other: true });
|
|
327
|
+
const max = Math.max(1, ...rows.map((r) => r.value));
|
|
328
|
+
|
|
329
|
+
const draw = (width) => {
|
|
330
|
+
const labelW = Math.min(200, Math.round(width * 0.38));
|
|
331
|
+
const x0 = labelW + 10;
|
|
332
|
+
const x1 = width - 36;
|
|
333
|
+
const rowH = 26;
|
|
334
|
+
const barH = 12;
|
|
335
|
+
const height = rows.length * rowH + 6;
|
|
336
|
+
const g = svg('svg', { class: 'chart-svg', width, height, viewBox: `0 0 ${width} ${height}`, role: 'img', 'aria-label': summary });
|
|
337
|
+
g.append(svg('line', { class: 'axis', x1: x0, x2: x0, y1: 0, y2: height - 4 }));
|
|
338
|
+
rows.forEach((r, i) => {
|
|
339
|
+
const cy = i * rowH + rowH / 2 + 2;
|
|
340
|
+
const w = (r.value / max) * (x1 - x0);
|
|
341
|
+
const parts = [
|
|
342
|
+
svg('rect', { class: 'hit', x: 0, y: cy - rowH / 2, width, height: rowH }),
|
|
343
|
+
svg('text', { class: 'row-label', x: labelW, y: cy + 4, 'text-anchor': 'end', text: fit(r.label, labelW) }),
|
|
344
|
+
w > 0 ? svg('path', { class: 'bar' + (r.other ? ' is-other' : ''), d: barPath(x0, cy - barH / 2, Math.max(w, 2), barH, true) }) : null,
|
|
345
|
+
svg('text', { class: 'value', x: x0 + w + 6, y: cy + 4, text: String(r.value) }),
|
|
346
|
+
];
|
|
347
|
+
const tipText = `${r.label}: ${r.value} 條 requirement`;
|
|
348
|
+
g.append(r.href ? svg('a', { href: r.href, 'data-tip': tipText }, parts) : svg('g', { 'data-tip': tipText }, parts));
|
|
349
|
+
});
|
|
350
|
+
return g;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const table = dataTable(['spec', 'requirement'], sorted.map((s) => [s.id, s.requirementCount]));
|
|
354
|
+
return figure({ title, summary, draw, table });
|
|
355
|
+
}
|