@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
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// Pure derivations from snapshot payloads: no DOM, no fetch, so vitest can
|
|
2
|
+
// import this file directly. Every value here is recomputed from what the
|
|
3
|
+
// project's files say on each render; nothing is remembered between renders
|
|
4
|
+
// except the line-colour slots (a display preference, not product state).
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The four stations every change passes, whatever its schema: the stations
|
|
8
|
+
* read only artifact completion, task progress and the derived phase, so a
|
|
9
|
+
* custom schema with different artifacts still lands on the same line.
|
|
10
|
+
*/
|
|
11
|
+
export const STATIONS = [
|
|
12
|
+
{ id: 'plan', label: 'plan', hint: '寫 artifact' },
|
|
13
|
+
{ id: 'apply', label: 'apply', hint: '勾 task' },
|
|
14
|
+
{ id: 'sync', label: 'sync', hint: '驗證 spec' },
|
|
15
|
+
{ id: 'archive', label: 'archive', hint: '歸檔' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const DAY_MS = 86400000;
|
|
19
|
+
export const DEFAULT_STALE_DAYS = 14;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Where a change whose metadata will not parse stands. Its counts are zeros,
|
|
23
|
+
* not measurements, so any station it was drawn at would be a fallback the
|
|
24
|
+
* files never said; the map parks it before the line starts instead.
|
|
25
|
+
*/
|
|
26
|
+
export const UNKNOWN_STATION = 'unknown';
|
|
27
|
+
|
|
28
|
+
/** Which station a change stands at. */
|
|
29
|
+
export function stationOf(change) {
|
|
30
|
+
if (change.unreadable) return UNKNOWN_STATION;
|
|
31
|
+
if (change.phase === 'archiving') return 'archive';
|
|
32
|
+
if (change.phase === 'ready') return 'sync';
|
|
33
|
+
const { done, total } = change.artifactSummary;
|
|
34
|
+
// `total > 0` mirrors deriveChangePhase: 0 === 0 must not promote a change
|
|
35
|
+
// whose schema could not be read past planning.
|
|
36
|
+
if (total > 0 && done === total) return 'apply';
|
|
37
|
+
return 'plan';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function stationIndex(id) {
|
|
41
|
+
return STATIONS.findIndex((s) => s.id === id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function stationLabel(id) {
|
|
45
|
+
const station = STATIONS.find((s) => s.id === id);
|
|
46
|
+
return station ? station.label : '無法判斷階段';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function idleDays(change, now) {
|
|
50
|
+
const then = new Date(change.lastModified).getTime();
|
|
51
|
+
if (Number.isNaN(then)) return 0;
|
|
52
|
+
return Math.max(0, Math.floor((now - then) / DAY_MS));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The one signal a change's marker shows, most urgent first.
|
|
57
|
+
*
|
|
58
|
+
* `blocked` artifacts are deliberately absent: blocked means "a dependency is
|
|
59
|
+
* not written yet", which is every fresh change's normal state (tasks waits on
|
|
60
|
+
* specs and design). Only metadata that will not parse stops a line.
|
|
61
|
+
*/
|
|
62
|
+
export function signalOf(change, staleDays, now) {
|
|
63
|
+
if (change.unreadable) return 'stop';
|
|
64
|
+
if (change.phase === 'ready' || change.phase === 'archiving') return 'call';
|
|
65
|
+
if (idleDays(change, now) > staleDays) return 'idle';
|
|
66
|
+
if (change.phase === 'applying') return 'running';
|
|
67
|
+
return 'moving';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* One word per marker, in the project's own terms: the map borrows a transit
|
|
72
|
+
* look, never transit words, so a label always names a tospec state.
|
|
73
|
+
* `running` and `moving` share the solid marker and so share its word; the
|
|
74
|
+
* flowing segment is what says "being worked on now". `parked` is a change in
|
|
75
|
+
* a project with no session: its files may have moved since the snapshot.
|
|
76
|
+
*/
|
|
77
|
+
export const SIGNAL_LABEL = {
|
|
78
|
+
stop: '無法解析',
|
|
79
|
+
call: '待處理',
|
|
80
|
+
idle: '閒置',
|
|
81
|
+
running: '進行中',
|
|
82
|
+
moving: '進行中',
|
|
83
|
+
parked: '離線',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A session's state as its mod last reported it. A session whose mod predates
|
|
88
|
+
* activity reports, or has not sent one yet, reads as idle rather than as
|
|
89
|
+
* working: claiming work nobody reported would be the worse error.
|
|
90
|
+
*/
|
|
91
|
+
export function sessionStatus(session) {
|
|
92
|
+
return session.status === 'working' || session.status === 'waiting' ? session.status : 'idle';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A project's state at a glance: its most pressing session, waiting first. */
|
|
96
|
+
export function projectActivity(project) {
|
|
97
|
+
if (!project.online || !project.sessions.length) return 'offline';
|
|
98
|
+
const statuses = project.sessions.map(sessionStatus);
|
|
99
|
+
if (statuses.includes('waiting')) return 'waiting';
|
|
100
|
+
if (statuses.includes('working')) return 'working';
|
|
101
|
+
return 'idle';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const ACTIVITY_LABEL = {
|
|
105
|
+
working: '工作中',
|
|
106
|
+
waiting: '等你回覆',
|
|
107
|
+
idle: '閒置',
|
|
108
|
+
offline: '離線',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** How long, in words: `3 分鐘`, `2 小時`, under a minute `不到 1 分鐘`. */
|
|
112
|
+
export function durationText(since, now) {
|
|
113
|
+
const then = since instanceof Date ? since.getTime() : new Date(since).getTime();
|
|
114
|
+
const mins = Number.isNaN(then) ? 0 : Math.floor((now - then) / 60000);
|
|
115
|
+
if (mins < 1) return '不到 1 分鐘';
|
|
116
|
+
if (mins < 60) return `${mins} 分鐘`;
|
|
117
|
+
const hours = Math.floor(mins / 60);
|
|
118
|
+
return hours < 24 ? `${hours} 小時` : `${Math.floor(hours / 24)} 天`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** What a session's open ask is about, in one line. */
|
|
122
|
+
export function askSummary(ask) {
|
|
123
|
+
if (!ask) return '';
|
|
124
|
+
if (ask.kind === 'permission') return `允許 ${ask.tool}${ask.target ? ' ' + ask.target : ''}?`;
|
|
125
|
+
return ask.questions.map((q) => q.question).join(' / ');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* What changed since the last look that the person should hear about: a
|
|
130
|
+
* session that began waiting on a question (or moved on to a new one), and a
|
|
131
|
+
* session whose turn just ended. `seen` maps `<key>/<session>` to the state
|
|
132
|
+
* last seen; the returned `seen` is the one to pass next time. A session not
|
|
133
|
+
* seen before is news only if it already waits: one that arrives idle never
|
|
134
|
+
* worked under the person's eyes.
|
|
135
|
+
*/
|
|
136
|
+
export function sessionAlerts(seen, projects) {
|
|
137
|
+
const next = new Map();
|
|
138
|
+
const alerts = [];
|
|
139
|
+
for (const project of projects) {
|
|
140
|
+
for (const session of project.sessions) {
|
|
141
|
+
const id = project.key + '/' + session.id;
|
|
142
|
+
const status = sessionStatus(session);
|
|
143
|
+
// Keyed by the ask too: a second question right after the first is news.
|
|
144
|
+
const state = status === 'waiting' && session.ask ? 'waiting:' + session.ask.id : status;
|
|
145
|
+
next.set(id, state);
|
|
146
|
+
const before = seen.get(id);
|
|
147
|
+
if (state === before) continue;
|
|
148
|
+
if (status === 'waiting') alerts.push({ kind: 'wait', project, session });
|
|
149
|
+
else if (status === 'idle' && before === 'working') alerts.push({ kind: 'idle', project, session });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { seen: next, alerts };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The words beside a session's dot: the tool it runs while working, the
|
|
157
|
+
* question while waiting, how long it has idled.
|
|
158
|
+
*/
|
|
159
|
+
export function sessionDetail(session, now) {
|
|
160
|
+
const status = sessionStatus(session);
|
|
161
|
+
if (status === 'working') return session.action || '';
|
|
162
|
+
if (status === 'waiting') return askSummary(session.ask);
|
|
163
|
+
return session.statusSince ? durationText(session.statusSince, now) : '';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The signal as drawn on a project's line: offline lines park their moving trains. */
|
|
167
|
+
export function lineSignal(signal, online) {
|
|
168
|
+
return !online && (signal === 'moving' || signal === 'running') ? 'parked' : signal;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The parse failure behind a stopped train, cut down to what fits a row: the
|
|
173
|
+
* file, the line when the parser names one, and the parser's own words.
|
|
174
|
+
*/
|
|
175
|
+
export function unreadableInfo(change) {
|
|
176
|
+
const raw = String(change.unreadable || '');
|
|
177
|
+
const line = /line (\d+)/i.exec(raw);
|
|
178
|
+
const colon = raw.indexOf(': ');
|
|
179
|
+
let reason = colon >= 0 ? raw.slice(colon + 2) : raw;
|
|
180
|
+
if (reason.length > 72) reason = reason.slice(0, 71) + '…';
|
|
181
|
+
return {
|
|
182
|
+
file: `tospec/changes/${change.name}/.tospec.yaml`,
|
|
183
|
+
summary: line ? `.tospec.yaml 第 ${line[1]} 行無法解析` : '.tospec.yaml 無法解析',
|
|
184
|
+
reason,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* What the change is missing to leave its station. A planning change names
|
|
190
|
+
* the artifacts that can be written now when its detail is known; without the
|
|
191
|
+
* detail it falls back to counts rather than guessing names.
|
|
192
|
+
*/
|
|
193
|
+
export function gapOf(change, detail) {
|
|
194
|
+
if (change.unreadable) return unreadableInfo(change).summary;
|
|
195
|
+
const station = stationOf(change);
|
|
196
|
+
if (station === 'archive') return 'sync 通過, 可 archive';
|
|
197
|
+
if (station === 'sync') return 'task 全部完成, 等 sync';
|
|
198
|
+
if (station === 'apply') {
|
|
199
|
+
const { completed, total } = change.taskProgress;
|
|
200
|
+
return total === 0 ? '沒有 task' : `task ${completed}/${total}`;
|
|
201
|
+
}
|
|
202
|
+
if (detail && Array.isArray(detail.artifacts)) {
|
|
203
|
+
const ready = detail.artifacts.filter((a) => a.status === 'ready').map((a) => a.id);
|
|
204
|
+
const stub = detail.artifacts.filter((a) => a.status === 'stub').map((a) => a.id);
|
|
205
|
+
if (stub.length) return '待補完 ' + stub.join(', ');
|
|
206
|
+
if (ready.length) return '可寫 ' + ready.join(', ');
|
|
207
|
+
}
|
|
208
|
+
const { done, total } = change.artifactSummary;
|
|
209
|
+
return `artifact ${done}/${total}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Cross-project items that need the user, most urgent first. Offline projects
|
|
214
|
+
* are not items: their line already says so, and nothing in them can change
|
|
215
|
+
* until a session returns.
|
|
216
|
+
*/
|
|
217
|
+
export function noticeItems(projects, dataByKey, now) {
|
|
218
|
+
// A session blocked on a question comes first: work stops until it is answered.
|
|
219
|
+
const rank = { wait: 0, stop: 1, call: 2, idle: 3, drift: 4 };
|
|
220
|
+
const items = [];
|
|
221
|
+
for (const project of projects) {
|
|
222
|
+
for (const session of project.sessions) {
|
|
223
|
+
if (sessionStatus(session) === 'waiting') {
|
|
224
|
+
items.push({ kind: 'wait', project, session, text: askSummary(session.ask) });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const data = dataByKey.get(project.key);
|
|
228
|
+
const overview = data && data.overview;
|
|
229
|
+
if (!overview) continue;
|
|
230
|
+
const staleDays = staleDaysOf(data);
|
|
231
|
+
for (const change of overview.changes) {
|
|
232
|
+
const signal = signalOf(change, staleDays, now);
|
|
233
|
+
if (signal === 'stop') {
|
|
234
|
+
const info = unreadableInfo(change);
|
|
235
|
+
items.push({ kind: 'stop', project, change, text: `${info.summary}: ${info.reason}`, hint: `修正 ${info.file} 後, 下一次快照就會恢復.` });
|
|
236
|
+
} else if (signal === 'call') {
|
|
237
|
+
items.push({ kind: 'call', project, change, text: gapOf(change) });
|
|
238
|
+
} else if (signal === 'idle') {
|
|
239
|
+
items.push({
|
|
240
|
+
kind: 'idle',
|
|
241
|
+
project,
|
|
242
|
+
change,
|
|
243
|
+
text: `閒置 ${idleDays(change, now)} 天, 目前在 ${stationLabel(stationOf(change))} 階段 (${gapOf(change, data.details && data.details.get(change.name))})`,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const ticket of overview.tickets || []) {
|
|
248
|
+
if (ticket.parseError) {
|
|
249
|
+
items.push({ kind: 'stop', project, ticket, text: 'ticket frontmatter 無法解析', hint: `修正 ${ticket.file} 的 frontmatter.` });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (data.activity && data.activity.available && data.activity.specDrift) {
|
|
253
|
+
items.push({ kind: 'drift', project, text: '近 7 天程式碼有變動, tospec/ 沒有' });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return items.sort(
|
|
257
|
+
(a, b) => rank[a.kind] - rank[b.kind] || a.project.name.localeCompare(b.project.name)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function staleDaysOf(data) {
|
|
262
|
+
return (data && data.activity && data.activity.staleDays) || DEFAULT_STALE_DAYS;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- time ------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* `20260713_123649` (archive and decision stamps), `2026-07-13T12:36:49` or
|
|
269
|
+
* `2026-07-13` (change metadata `created`, new and older changes) as a local
|
|
270
|
+
* Date; anything else through Date, or null.
|
|
271
|
+
*/
|
|
272
|
+
export function parseLooseDate(value) {
|
|
273
|
+
if (!value) return null;
|
|
274
|
+
const stamp = /^(\d{4})(\d{2})(\d{2})(?:_(\d{2})(\d{2})(\d{2}))?$/.exec(value);
|
|
275
|
+
if (stamp) {
|
|
276
|
+
const [, y, mo, d, h = '0', mi = '0', s = '0'] = stamp;
|
|
277
|
+
return new Date(+y, +mo - 1, +d, +h, +mi, +s);
|
|
278
|
+
}
|
|
279
|
+
// A bare ISO date parses as UTC midnight, which lands on the previous day
|
|
280
|
+
// west of Greenwich; read it as the local calendar date it names. The
|
|
281
|
+
// zoneless datetime is read here too rather than left to Date, so neither
|
|
282
|
+
// spelling depends on how the engine treats a value without an offset.
|
|
283
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}):(\d{2}))?$/.exec(value);
|
|
284
|
+
if (iso) {
|
|
285
|
+
const [, y, mo, d, h = '0', mi = '0', s = '0'] = iso;
|
|
286
|
+
return new Date(+y, +mo - 1, +d, +h, +mi, +s);
|
|
287
|
+
}
|
|
288
|
+
const parsed = new Date(value);
|
|
289
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function relTime(value, now) {
|
|
293
|
+
const date = value instanceof Date ? value : parseLooseDate(value);
|
|
294
|
+
if (!date) return '';
|
|
295
|
+
const mins = Math.floor((now - date.getTime()) / 60000);
|
|
296
|
+
if (mins < 1) return '剛剛';
|
|
297
|
+
if (mins < 60) return `${mins} 分鐘前`;
|
|
298
|
+
const hours = Math.floor(mins / 60);
|
|
299
|
+
if (hours < 24) return `${hours} 小時前`;
|
|
300
|
+
const days = Math.floor(hours / 24);
|
|
301
|
+
if (days <= 30) return `${days} 天前`;
|
|
302
|
+
return formatDate(date);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function formatDate(date) {
|
|
306
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
307
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function formatStamp(value) {
|
|
311
|
+
const date = parseLooseDate(value);
|
|
312
|
+
if (!date) return value || '';
|
|
313
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
314
|
+
return `${formatDate(date)} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Local midnight of the Monday on or before `date`. */
|
|
318
|
+
export function weekStart(date) {
|
|
319
|
+
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
320
|
+
const offset = (d.getDay() + 6) % 7;
|
|
321
|
+
d.setDate(d.getDate() - offset);
|
|
322
|
+
return d;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Archived changes per week for the last `weeks` weeks, oldest first. */
|
|
326
|
+
export function weeklyArchiveCounts(archivedChanges, now, weeks = 12) {
|
|
327
|
+
const current = weekStart(new Date(now));
|
|
328
|
+
const buckets = [];
|
|
329
|
+
for (let i = weeks - 1; i >= 0; i--) {
|
|
330
|
+
const start = new Date(current);
|
|
331
|
+
start.setDate(start.getDate() - i * 7);
|
|
332
|
+
buckets.push({ start, count: 0 });
|
|
333
|
+
}
|
|
334
|
+
const first = buckets[0].start.getTime();
|
|
335
|
+
for (const change of archivedChanges) {
|
|
336
|
+
const date = parseLooseDate(change.stamp);
|
|
337
|
+
if (!date || date.getTime() < first) continue;
|
|
338
|
+
const index = Math.floor((weekStart(date).getTime() - first) / (7 * DAY_MS) + 0.5);
|
|
339
|
+
if (index >= 0 && index < buckets.length) buckets[index].count += 1;
|
|
340
|
+
}
|
|
341
|
+
return buckets;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// --- line colours ------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
export const LINE_SLOTS = 6;
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Gives each project a line-colour slot that survives reorders and restarts:
|
|
350
|
+
* a project keeps the slot it was first given, and a new one takes the lowest
|
|
351
|
+
* slot no listed project holds. Colour follows the project, never its rank in
|
|
352
|
+
* a list that re-sorts on every push. Past six projects slots repeat; the
|
|
353
|
+
* project name beside every line still identifies it.
|
|
354
|
+
*/
|
|
355
|
+
export function assignLineSlots(keys, saved) {
|
|
356
|
+
const result = new Map();
|
|
357
|
+
const taken = new Set();
|
|
358
|
+
for (const key of keys) {
|
|
359
|
+
const slot = saved[key];
|
|
360
|
+
if (Number.isInteger(slot) && slot >= 1 && slot <= LINE_SLOTS && !taken.has(slot)) {
|
|
361
|
+
result.set(key, slot);
|
|
362
|
+
taken.add(slot);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
let overflow = 0;
|
|
366
|
+
for (const key of keys) {
|
|
367
|
+
if (result.has(key)) continue;
|
|
368
|
+
let slot = 1;
|
|
369
|
+
while (taken.has(slot) && slot <= LINE_SLOTS) slot++;
|
|
370
|
+
if (slot > LINE_SLOTS) slot = (overflow++ % LINE_SLOTS) + 1;
|
|
371
|
+
result.set(key, slot);
|
|
372
|
+
taken.add(slot);
|
|
373
|
+
}
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// DOM helpers shared by every view. All text goes in as textContent, never
|
|
2
|
+
// innerHTML, so snapshot data cannot inject markup; the one innerHTML in the
|
|
3
|
+
// app is markdown the project side already sanitized (core/markdown-render.ts).
|
|
4
|
+
|
|
5
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
6
|
+
|
|
7
|
+
/** Minimal element builder; `text` sets textContent, null props and children are skipped. */
|
|
8
|
+
export function el(tag, props = {}, children = []) {
|
|
9
|
+
const node = document.createElement(tag);
|
|
10
|
+
setProps(node, props);
|
|
11
|
+
appendChildren(node, children);
|
|
12
|
+
return node;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function svg(tag, props = {}, children = []) {
|
|
16
|
+
const node = document.createElementNS(SVG_NS, tag);
|
|
17
|
+
setProps(node, props);
|
|
18
|
+
appendChildren(node, children);
|
|
19
|
+
return node;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function setProps(node, props) {
|
|
23
|
+
for (const [key, value] of Object.entries(props)) {
|
|
24
|
+
if (value == null || value === false) continue;
|
|
25
|
+
if (key === 'class') node.setAttribute('class', value);
|
|
26
|
+
else if (key === 'text') node.textContent = value;
|
|
27
|
+
else node.setAttribute(key, value === true ? '' : String(value));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function appendChildren(node, children) {
|
|
32
|
+
for (const child of [].concat(children)) {
|
|
33
|
+
if (child == null || child === false) continue;
|
|
34
|
+
node.append(child.nodeType ? child : document.createTextNode(String(child)));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Points a subtree at a project's line colour. Set through the CSSOM rather
|
|
40
|
+
* than a style attribute, and as a reference to the theme variable rather than
|
|
41
|
+
* a hex value, so a theme switch recolours every line without a re-render.
|
|
42
|
+
*/
|
|
43
|
+
export function paintLine(node, slot) {
|
|
44
|
+
node.style.setProperty('--line', `var(--line-${slot})`);
|
|
45
|
+
// Spelled out here, not derived from --line in the stylesheet: a custom
|
|
46
|
+
// property that reads another resolves where it is declared, so a :root
|
|
47
|
+
// definition would mix the root's colour, never this line's.
|
|
48
|
+
node.style.setProperty('--line-soft', `color-mix(in oklab, var(--line-${slot}) 14%, var(--surface))`);
|
|
49
|
+
return node;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- icons -------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The marker a change carries on its line. Shape alone tells the signals
|
|
56
|
+
* apart, so the map still reads without colour (CVD, forced colours, print).
|
|
57
|
+
*/
|
|
58
|
+
export function marker(signal, size = 14) {
|
|
59
|
+
const box = { width: size, height: size, viewBox: '0 0 14 14', 'aria-hidden': 'true', focusable: 'false' };
|
|
60
|
+
const cls = `marker marker-${signal}`;
|
|
61
|
+
if (signal === 'stop') {
|
|
62
|
+
return svg('svg', { ...box, class: cls }, [svg('rect', { x: 1.5, y: 1.5, width: 11, height: 11, rx: 1.5 })]);
|
|
63
|
+
}
|
|
64
|
+
if (signal === 'call') {
|
|
65
|
+
return svg('svg', { ...box, class: cls }, [svg('circle', { cx: 7, cy: 7, r: 5 })]);
|
|
66
|
+
}
|
|
67
|
+
if (signal === 'idle') {
|
|
68
|
+
return svg('svg', { ...box, class: cls }, [svg('circle', { cx: 7, cy: 7, r: 5 })]);
|
|
69
|
+
}
|
|
70
|
+
if (signal === 'parked') {
|
|
71
|
+
return svg('svg', { ...box, class: cls }, [
|
|
72
|
+
svg('rect', { x: 3, y: 2, width: 3, height: 10, rx: 1 }),
|
|
73
|
+
svg('rect', { x: 8, y: 2, width: 3, height: 10, rx: 1 }),
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
return svg('svg', { ...box, class: cls }, [svg('circle', { cx: 7, cy: 7, r: 5.5 })]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A session's (or a project's) live state. Shape and motion carry it, as the
|
|
81
|
+
* markers do: a pulsing disc works, a half-filled one waits on the user, a
|
|
82
|
+
* ring idles, a dash is offline.
|
|
83
|
+
*/
|
|
84
|
+
export function activityDot(activity) {
|
|
85
|
+
return el('span', { class: 'activity-dot activity-' + activity, 'aria-hidden': 'true' });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const ICON_PATHS = {
|
|
89
|
+
refresh: 'M13.5 3.5v3.2h-3.2M2.5 12.5V9.3h3.2M3.2 6.2a5 5 0 0 1 9-1.6l1.3 1.1M12.8 9.8a5 5 0 0 1-9 1.6L2.5 10.3',
|
|
90
|
+
chevron: 'M4 6l4 4 4-4',
|
|
91
|
+
back: 'M10 3L5 8l5 5',
|
|
92
|
+
search: 'M7 12A5 5 0 1 0 7 2a5 5 0 0 0 0 10zM10.6 10.6L14 14',
|
|
93
|
+
lock: 'M4.5 7V5.5a3.5 3.5 0 0 1 7 0V7M3.5 7h9v6.5h-9z',
|
|
94
|
+
menu: 'M2.5 4h11M2.5 8h11M2.5 12h11',
|
|
95
|
+
overview: 'M2.5 2.5h4.5v4.5H2.5zM9 2.5h4.5v4.5H9zM2.5 9h4.5v4.5H2.5zM9 9h4.5v4.5H9z',
|
|
96
|
+
copy: 'M5.5 5.5h8v8h-8zM10.5 5.5v-3h-8v8h3',
|
|
97
|
+
close: 'M3.5 3.5l9 9M12.5 3.5l-9 9',
|
|
98
|
+
log: 'M2.5 3.5h11M2.5 6.5h11M2.5 9.5h7M2.5 12.5h9',
|
|
99
|
+
bell: 'M4 11.5V7a4 4 0 0 1 8 0v4.5l1.2 1.3H2.8zM6.6 14a1.5 1.5 0 0 0 2.8 0',
|
|
100
|
+
'bell-off': 'M4 11.5V7a4 4 0 0 1 8 0v4.5l1.2 1.3H2.8zM6.6 14a1.5 1.5 0 0 0 2.8 0M2 2l12 12',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export function icon(name, size = 16) {
|
|
104
|
+
return svg('svg', {
|
|
105
|
+
class: 'icon icon-' + name,
|
|
106
|
+
width: size,
|
|
107
|
+
height: size,
|
|
108
|
+
viewBox: '0 0 16 16',
|
|
109
|
+
'aria-hidden': 'true',
|
|
110
|
+
focusable: 'false',
|
|
111
|
+
}, [svg('path', { d: ICON_PATHS[name] })]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The project's line badge: a short bar of its colour beside its name. */
|
|
115
|
+
export function lineBadge(name, slot, tag = 'span') {
|
|
116
|
+
return paintLine(el(tag, { class: 'line-badge' }, [el('span', { class: 'line-swatch', 'aria-hidden': 'true' }), el('span', { class: 'line-name', text: name })]), slot);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function note(text, cls = 'note') {
|
|
120
|
+
return el('p', { class: cls, text });
|
|
121
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Hash routes. The project lives in the hash, not the query string, so moving
|
|
2
|
+
// between projects is a route change rather than a page load.
|
|
3
|
+
|
|
4
|
+
const enc = encodeURIComponent;
|
|
5
|
+
|
|
6
|
+
export const routes = {
|
|
7
|
+
home: () => '#/',
|
|
8
|
+
project: (key) => `#/p/${key}`,
|
|
9
|
+
change: (key, name) => `#/p/${key}/changes/${enc(name)}`,
|
|
10
|
+
spec: (key, id) => `#/p/${key}/specs/${enc(id)}`,
|
|
11
|
+
decisions: (key, file) => (file ? `#/p/${key}/decisions/${enc(file)}` : `#/p/${key}/decisions`),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** `#/p/<key>/changes/<name>` -> { view: 'change', key, name }. Unknown hashes fall back home. */
|
|
15
|
+
export function parseRoute(hash) {
|
|
16
|
+
const parts = hash.replace(/^#\/?/, '').split('/').filter(Boolean);
|
|
17
|
+
if (parts[0] !== 'p' || !parts[1]) return { view: 'home' };
|
|
18
|
+
const key = parts[1];
|
|
19
|
+
const rest = parts.slice(3).map((p) => {
|
|
20
|
+
try {
|
|
21
|
+
return decodeURIComponent(p);
|
|
22
|
+
} catch {
|
|
23
|
+
return p;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
switch (parts[2]) {
|
|
27
|
+
case undefined:
|
|
28
|
+
return { view: 'project', key };
|
|
29
|
+
case 'changes':
|
|
30
|
+
return rest[0] ? { view: 'change', key, name: rest[0] } : { view: 'project', key };
|
|
31
|
+
case 'specs':
|
|
32
|
+
return rest[0] ? { view: 'spec', key, id: rest[0] } : { view: 'project', key };
|
|
33
|
+
case 'decisions':
|
|
34
|
+
return { view: 'decisions', key, file: rest[0] || null };
|
|
35
|
+
default:
|
|
36
|
+
return { view: 'project', key };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Bookmarks from the single-project dashboard read `?project=<key>#/changes/x`.
|
|
42
|
+
* Returns the equivalent hash route, or null when the URL is already current.
|
|
43
|
+
*/
|
|
44
|
+
export function legacyRoute(search, hash) {
|
|
45
|
+
const key = new URLSearchParams(search).get('project');
|
|
46
|
+
if (!key) return null;
|
|
47
|
+
const inner = hash.replace(/^#\/?/, '');
|
|
48
|
+
if (inner.startsWith('changes/') || inner.startsWith('specs/') || inner.startsWith('decisions')) {
|
|
49
|
+
return `#/p/${key}/${inner}`;
|
|
50
|
+
}
|
|
51
|
+
return `#/p/${key}`;
|
|
52
|
+
}
|