@erclx/canon 5.77.0 → 5.79.0

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.
Files changed (125) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/app-drive/REQUIREMENT.md +12 -0
  4. package/claude/skills/app-drive/SKILL.md +44 -8
  5. package/claude/skills/app-drive/references/config.md +18 -8
  6. package/claude/skills/app-drive/references/drive-record.md +4 -0
  7. package/claude/skills/auto-ship/REQUIREMENT.md +1 -1
  8. package/claude/skills/auto-ship/SKILL.md +5 -4
  9. package/claude/skills/auto-ship/references/draft-mark.md +33 -15
  10. package/claude/skills/auto-ship/references/failure-recovery.md +4 -2
  11. package/claude/skills/auto-ship/references/ui-checklist.md +9 -1
  12. package/claude/skills/code-craft/REQUIREMENT.md +1 -1
  13. package/claude/skills/code-craft/SKILL.md +7 -0
  14. package/claude/skills/code-craft/references/adopted.md +8 -0
  15. package/claude/skills/git-followup/SKILL.md +1 -0
  16. package/claude/skills/git-pr/REQUIREMENT.md +2 -0
  17. package/claude/skills/git-pr/SKILL.md +2 -0
  18. package/claude/skills/git-pr/references/final-command.md +34 -2
  19. package/claude/skills/git-pr/references/pr.md +1 -1
  20. package/claude/skills/git-ship/REQUIREMENT.md +2 -2
  21. package/claude/skills/git-ship/SKILL.md +16 -11
  22. package/claude/skills/git-ship/references/ci-watch.md +28 -4
  23. package/claude/skills/markdown-craft/references/markdown.md +4 -4
  24. package/claude/skills/plan-feature/references/plan.md +2 -0
  25. package/claude/skills/pr-evidence/SKILL.md +6 -1
  26. package/claude/skills/pr-evidence/references/evidence.md +1 -0
  27. package/claude/skills/pr-evidence/references/reel.md +9 -1
  28. package/claude/skills/review-address/SKILL.md +2 -1
  29. package/claude/skills/review-craft/references/ui.md +13 -1
  30. package/claude/skills/review-pr/references/body-variants.md +2 -0
  31. package/claude/skills/review-pr/references/close-out.md +2 -0
  32. package/claude/skills/review-pr/references/key-changes.md +2 -1
  33. package/claude/skills/review-pr/references/post.md +1 -0
  34. package/claude/skills/review-ui/REQUIREMENT.md +1 -0
  35. package/claude/skills/review-ui/SKILL.md +2 -2
  36. package/claude/skills/review-ui/references/driving.md +32 -4
  37. package/claude/skills/review-ui/references/post.md +6 -2
  38. package/claude/skills/role-orchestrator/REQUIREMENT.md +1 -1
  39. package/claude/skills/role-orchestrator/SKILL.md +4 -4
  40. package/claude/skills/role-orchestrator/references/orchestrator-hand-launch.md +2 -0
  41. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
  42. package/claude/skills/role-orchestrator/references/orchestrator-limit-stops.md +47 -0
  43. package/claude/skills/role-orchestrator/references/orchestrator-output.md +24 -1
  44. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +3 -2
  45. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +35 -4
  46. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +1 -1
  47. package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +3 -1
  48. package/claude/skills/role-worker/SKILL.md +2 -1
  49. package/claude/skills/task-board/SKILL.md +1 -0
  50. package/claude/skills/task-board/references/tasks.md +1 -0
  51. package/claude/skills/test-craft/SKILL.md +1 -0
  52. package/claude/skills/ui-checklist/REQUIREMENT.md +1 -1
  53. package/claude/skills/ui-checklist/SKILL.md +2 -1
  54. package/claude/skills/ui-craft/references/capture.md +16 -0
  55. package/claude/skills/ui-film/REQUIREMENT.md +2 -2
  56. package/claude/skills/ui-film/SKILL.md +6 -1
  57. package/claude/skills/ui-film/references/compose-craft.md +68 -11
  58. package/claude/skills/ui-film/references/config.md +2 -2
  59. package/claude/skills/ui-film/references/handoff.md +1 -1
  60. package/claude/skills/ui-film/references/harness.md +71 -0
  61. package/claude/skills/ui-film/references/take-craft.md +42 -17
  62. package/docs/workflow/ai-workflow.md +1 -1
  63. package/docs/workflow/operating-model.md +6 -5
  64. package/docs/workflow/parallel-features.md +2 -1
  65. package/docs/workflow/visual-design-workflow.md +2 -1
  66. package/governance/ledger/skills/code-craft/SKILL.md +9 -0
  67. package/governance/ledger/skills/test-craft/SKILL.md +1 -0
  68. package/governance/ledger/skills/ui-craft/capture.md +18 -5
  69. package/package.json +4 -3
  70. package/src/commands/pr.ts +255 -46
  71. package/src/commands/sessions.ts +88 -6
  72. package/src/commands/take.ts +12 -1
  73. package/src/commands/tasks.ts +41 -2
  74. package/src/design/components.ts +5 -107
  75. package/src/design/favicon.ts +21 -17
  76. package/src/design/render.ts +9 -27
  77. package/src/design/template.ts +42 -0
  78. package/src/design/templates/favicon.svg +17 -0
  79. package/src/design/templates/hand-drawn-figure.css +54 -0
  80. package/src/design/templates/preview.html +27 -0
  81. package/src/design/templates/scrollbar.css +29 -0
  82. package/src/design/templates/status.css +24 -0
  83. package/src/gate/embedded.ts +400 -0
  84. package/src/gate/stages.ts +8 -0
  85. package/src/git/files.ts +5 -2
  86. package/src/indexes/frontmatter.ts +27 -6
  87. package/src/indexes/render.ts +21 -3
  88. package/src/markdown/colons.ts +14 -4
  89. package/src/markdown/heading-case.ts +9 -2
  90. package/src/pr/bijection.ts +8 -0
  91. package/src/pr/evidence.ts +81 -13
  92. package/src/pr/frames.ts +1 -6
  93. package/src/pr/health.ts +63 -15
  94. package/src/pr/lift.ts +77 -9
  95. package/src/pr/links.ts +160 -0
  96. package/src/pr/local.ts +17 -4
  97. package/src/pr/paths.ts +20 -2
  98. package/src/pr/rest.ts +2 -0
  99. package/src/pr/review-scope.ts +28 -0
  100. package/src/pr/review-state.ts +5 -5
  101. package/src/pr/waiver.ts +45 -0
  102. package/src/sessions/stop.ts +145 -0
  103. package/src/sessions/transfer/bundle.ts +1 -1
  104. package/src/take/camera.ts +337 -208
  105. package/src/take/encode.ts +16 -42
  106. package/src/take/film.ts +7 -1
  107. package/src/take/frame-clock.ts +46 -0
  108. package/src/take/log.ts +43 -2
  109. package/src/take/perform.ts +85 -13
  110. package/src/take/plan.ts +26 -5
  111. package/src/take/reel.ts +30 -5
  112. package/src/take/screencast.ts +223 -13
  113. package/src/take/stepper.ts +24 -4
  114. package/src/tasks/holders.ts +198 -0
  115. package/src/tasks/validate.ts +148 -0
  116. package/src/commands/fixtures/gh.sh +0 -64
  117. package/src/take/fixtures/busy.html +0 -107
  118. package/src/take/fixtures/fade.html +0 -49
  119. package/src/take/fixtures/fetch.html +0 -38
  120. package/src/take/fixtures/input.html +0 -138
  121. package/src/take/zoom/NOTICE.md +0 -45
  122. package/src/take/zoom/math-utils.ts +0 -69
  123. package/src/take/zoom/region-utils.ts +0 -55
  124. package/src/take/zoom/spring.ts +0 -133
  125. /package/src/sessions/{transfer/transcript.ts → transcript.ts} +0 -0
@@ -1,6 +1,7 @@
1
1
  import { existsSync, statSync } from 'node:fs'
2
2
  import { readdir, readFile } from 'node:fs/promises'
3
3
  import { join, resolve } from 'node:path'
4
+ import { readFrontmatterSource } from '@/indexes/frontmatter'
4
5
  import { codeSpans, unbrokenRun } from '@/markdown/tables'
5
6
  import {
6
7
  archiveDir,
@@ -21,6 +22,8 @@ import {
21
22
  renderedLength,
22
23
  WAITING_LIMIT,
23
24
  } from '@/tasks/board'
25
+ import { branchOfPlan } from '@/tasks/branch'
26
+ import type { HolderReader, LiveReaders, MergedReader } from '@/tasks/holders'
24
27
  import { gitTrunkReader, type TrunkReader } from '@/tasks/trunk'
25
28
 
26
29
  const ORDERING_FILE = 'priority.md'
@@ -61,6 +64,7 @@ export const FINDING_KINDS = [
61
64
  'row-untabled',
62
65
  'row-misordered',
63
66
  'row-unranked',
67
+ 'row-held',
64
68
  'touches-unstated',
65
69
  'touches-stale',
66
70
  'columns-mismatched',
@@ -69,6 +73,7 @@ export const FINDING_KINDS = [
69
73
  'blocker-unresolved',
70
74
  'blocker-declined',
71
75
  'sessions-misshapen',
76
+ 'frontmatter-invalid',
72
77
  ] as const
73
78
 
74
79
  export type FindingKind = (typeof FINDING_KINDS)[number]
@@ -177,6 +182,18 @@ export interface BoardRow {
177
182
  readonly cells: readonly string[]
178
183
  }
179
184
 
185
+ /**
186
+ * What each source behind the held-row check answered. `unread` is a source the
187
+ * run asked and could not read, so a board carrying no `row-held` finding beside
188
+ * it is untested rather than clean. `not-asked` is a run without the live read.
189
+ */
190
+ export type LiveState = 'read' | 'unread' | 'not-asked'
191
+
192
+ export interface LiveReading {
193
+ readonly sessions: LiveState
194
+ readonly pulls: LiveState
195
+ }
196
+
180
197
  export interface ValidateReport {
181
198
  readonly ok: true
182
199
  readonly rows: number
@@ -189,6 +206,7 @@ export interface ValidateReport {
189
206
  readonly shared: readonly SharedFiles[]
190
207
  readonly wide: readonly WideToken[]
191
208
  readonly unplaced: readonly Unplaced[]
209
+ readonly live: LiveReading
192
210
  }
193
211
 
194
212
  export interface ValidateRefused {
@@ -877,6 +895,34 @@ async function checkSessions(
877
895
  return found.filter((finding): finding is Finding => finding !== undefined)
878
896
  }
879
897
 
898
+ /**
899
+ * Reports a task file whose frontmatter block will not parse, which the index
900
+ * regeneration would otherwise surface only after the row was placed. A file
901
+ * with no block is left alone, since the index walker owns that report.
902
+ */
903
+ async function checkFrontmatter(
904
+ stems: readonly string[],
905
+ dir: string,
906
+ ): Promise<Finding[]> {
907
+ const found = await Promise.all(
908
+ stems.map(async (stem): Promise<Finding | undefined> => {
909
+ const reading = readFrontmatterSource(
910
+ await readFile(join(dir, `${stem}.md`), 'utf8'),
911
+ )
912
+ if (reading.kind !== 'invalid') return undefined
913
+
914
+ return {
915
+ kind: 'frontmatter-invalid',
916
+ group: undefined,
917
+ subject: stem,
918
+ message: `frontmatter is not valid YAML: ${reading.message}. Double any apostrophe inside the single-quoted title.`,
919
+ }
920
+ }),
921
+ )
922
+
923
+ return found.filter((finding): finding is Finding => finding !== undefined)
924
+ }
925
+
880
926
  /**
881
927
  * Tests the claim a group name makes about a plan. `## Needs a plan` says none
882
928
  * is live and `## Up next` says one is written, and neither group has a `Plan`
@@ -1611,6 +1657,95 @@ export async function groupRuleFindings(
1611
1657
  ]
1612
1658
  }
1613
1659
 
1660
+ /**
1661
+ * Reports a row outside `## Run now` whose task a live session or an open pull
1662
+ * request holds, tied through the branch each live plan on its `Plan:` line
1663
+ * derives. A row whose worker renamed its branch has nothing to match, and an
1664
+ * archived plan's branch is finished work, so neither is reported.
1665
+ *
1666
+ * A session outlives its merge, so a session still on a branch a merged pull
1667
+ * request names is finished work too, until the merge reaches this clone and the
1668
+ * hook archives the row. A merged read that fails leaves the hold standing, since
1669
+ * the session is still there, and reads `pulls` as unread, since that hold may be
1670
+ * finished work the open list could not rule out.
1671
+ */
1672
+ async function checkHeld(
1673
+ rows: readonly BoardRow[],
1674
+ live: LiveRead,
1675
+ dir: string,
1676
+ root: string,
1677
+ ): Promise<{ readonly findings: Finding[]; readonly mergedUnread: boolean }> {
1678
+ let mergedUnread = false
1679
+
1680
+ const holdersOf = async (branch: string): Promise<string[]> => {
1681
+ const session = live.sessions?.get(branch)
1682
+ const merged = session ? await live.merged?.(branch) : false
1683
+ if (merged === undefined) mergedUnread = true
1684
+
1685
+ return [session && !merged ? session : undefined, live.pulls?.get(branch)]
1686
+ .filter((name): name is string => name !== undefined)
1687
+ .map((name) => `${name} on ${branch}`)
1688
+ }
1689
+
1690
+ const found = await Promise.all(
1691
+ rows.map(async (row): Promise<Finding | undefined> => {
1692
+ if (row.group === 'Run now' || !row.stem) return undefined
1693
+ const file = join(dir, `${row.stem}.md`)
1694
+ if (!existsSync(file)) return undefined
1695
+
1696
+ const targets = readPlanTargets(await readFile(file, 'utf8'))
1697
+ const branches = targets.flatMap((target) => {
1698
+ const path = target.split('#')[0] || target
1699
+ return resolveLivePlan(path, dir, root) === undefined
1700
+ ? []
1701
+ : [branchOfPlan(path)]
1702
+ })
1703
+ const held = (await Promise.all(branches.map(holdersOf))).flat()
1704
+ if (held.length === 0) return undefined
1705
+
1706
+ return {
1707
+ kind: 'row-held',
1708
+ group: row.group,
1709
+ subject: row.stem,
1710
+ message: `sits under ${row.group} and is held by ${held.join(' and ')}. A row with a worker belongs under Run now.`,
1711
+ }
1712
+ }),
1713
+ )
1714
+
1715
+ return {
1716
+ findings: found.filter(
1717
+ (finding): finding is Finding => finding !== undefined,
1718
+ ),
1719
+ mergedUnread,
1720
+ }
1721
+ }
1722
+
1723
+ /** What each source answered, `undefined` where it was not asked or failed. */
1724
+ interface LiveRead {
1725
+ readonly reading: LiveReading
1726
+ readonly sessions?: Awaited<ReturnType<HolderReader>>
1727
+ readonly pulls?: Awaited<ReturnType<HolderReader>>
1728
+ readonly merged?: MergedReader
1729
+ }
1730
+
1731
+ /** Takes each source once, since a failed read answers undefined rather than throws. */
1732
+ async function readLive(live: LiveReaders | undefined): Promise<LiveRead> {
1733
+ if (!live) {
1734
+ return { reading: { sessions: 'not-asked', pulls: 'not-asked' } }
1735
+ }
1736
+
1737
+ const stateOf = (read: Awaited<ReturnType<HolderReader>>): LiveState =>
1738
+ read ? 'read' : 'unread'
1739
+ const [sessions, pulls] = await Promise.all([live.sessions(), live.pulls()])
1740
+
1741
+ return {
1742
+ reading: { sessions: stateOf(sessions), pulls: stateOf(pulls) },
1743
+ sessions,
1744
+ pulls,
1745
+ merged: live.merged,
1746
+ }
1747
+ }
1748
+
1614
1749
  function refuse(reason: ValidateRefusal, message: string): ValidateRefused {
1615
1750
  return { ok: false, reason, message }
1616
1751
  }
@@ -1618,6 +1753,12 @@ function refuse(reason: ValidateRefusal, message: string): ValidateRefused {
1618
1753
  export interface ValidateOptions {
1619
1754
  /** Overridden by tests, which supply the trunk rather than reaching for git. */
1620
1755
  readonly trunk?: TrunkReader
1756
+ /**
1757
+ * The roster and GitHub reads behind the held-row check. Absent by default,
1758
+ * since the board's write hook runs this on every edit and a network read
1759
+ * there costs every write.
1760
+ */
1761
+ readonly live?: LiveReaders
1621
1762
  }
1622
1763
 
1623
1764
  /**
@@ -1668,6 +1809,8 @@ export async function validateBoard(
1668
1809
  ? await listTaskStems(declinedPath)
1669
1810
  : []
1670
1811
  const parked = await checkParked(rows, root, trunk)
1812
+ const live = await readLive(options.live)
1813
+ const held = await checkHeld(rows, live, dir, root)
1671
1814
 
1672
1815
  const findings = [
1673
1816
  ...shapeFindings,
@@ -1675,10 +1818,12 @@ export async function validateBoard(
1675
1818
  ...(await groupRuleFindings(rows, root)),
1676
1819
  ...(await checkPlanAgreement(rows, dir, root)),
1677
1820
  ...(await checkSessions(rows, dir)),
1821
+ ...(await checkFrontmatter(stems, dir)),
1678
1822
  ...(await checkStaleTouches(rows, root)),
1679
1823
  ...checkCellWidth(rows),
1680
1824
  ...checkOrdering(rows),
1681
1825
  ...parked.findings,
1826
+ ...held.findings,
1682
1827
  ]
1683
1828
 
1684
1829
  return {
@@ -1693,5 +1838,8 @@ export async function validateBoard(
1693
1838
  shared: checkSharedFiles(rows),
1694
1839
  wide: checkWideTokens(rows),
1695
1840
  unplaced: checkUnplaced(rows, backlog, stems),
1841
+ live: held.mergedUnread
1842
+ ? { ...live.reading, pulls: 'unread' }
1843
+ : live.reading,
1696
1844
  }
1697
1845
  }
@@ -1,64 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Fake gh for src/commands/pr.test.ts. Reads FAKE_GH_FX, FAKE_GH_COMMENTS, and FAKE_GH_API_LOG.
3
- set -o pipefail
4
- fx="$FAKE_GH_FX"
5
- f=.
6
- p=
7
- for a in "$@"; do
8
- [ "$p" = --jq ] && f=$a
9
- p=$a
10
- done
11
- case "$*" in
12
- *graphql* | "pr "* | "repo view"*) exit 1 ;;
13
- api*pulls/7/files*)
14
- echo "$*" >>"$fx/files-calls.log"
15
- [ -f "$fx/files-fail" ] && exit 1
16
- {
17
- if [ -f "$fx/files.tsv" ]; then
18
- cat "$fx/files.tsv"
19
- else
20
- git diff --name-status --no-renames main...HEAD | sed -e 's/^A/added/' -e 's/^M/modified/' -e 's/^D/removed/'
21
- fi
22
- } | jq -Rn '[inputs | select(length > 0) | split("\t") | {status: .[0], filename: .[1]}]' | jq -r "$f"
23
- ;;
24
- api*git/trees/*)
25
- [ -f "$fx/base-tree.json" ] || exit 1
26
- jq -r "$f" "$fx/base-tree.json"
27
- ;;
28
- api*contents/*)
29
- a="$*"
30
- p=${a##*contents/}
31
- p=${p%%\?*}
32
- r=${a##*ref=}
33
- [ -f "$fx/img/$r-$(basename "$p")" ] && cat "$fx/img/$r-$(basename "$p")" || exit 1
34
- ;;
35
- api*compare/*)
36
- if [ -f "$fx/merge-base" ]; then cat "$fx/merge-base"; else git merge-base main HEAD; fi
37
- ;;
38
- "api -X PUT repos/{owner}/{repo}/pulls/7/reviews/"*)
39
- id=${4##*/}
40
- echo "$id" >>"$fx/review-puts.log"
41
- [ -f "$fx/review-put-fail-$id" ] && exit 1
42
- printf '%s\n' "${@: -1}" >>"$fx/review-bodies.log"
43
- echo "{}"
44
- ;;
45
- api*pulls/7/reviews*) [ -f "$fx/ui-reviews.json" ] && jq -r "$f" "$fx/ui-reviews.json" ;;
46
- api*issues/7/comments*) jq '[.comments[] | {html_url: .url, body}]' "$FAKE_GH_COMMENTS" | jq -r "$f" ;;
47
- "api -X PATCH repos/{owner}/{repo}/pulls/7 "*)
48
- [ -f "$fx/body-fail" ] && exit 1
49
- printf '%s' "${@: -1}" >"$fx/body-write.log"
50
- echo "{}"
51
- ;;
52
- "api repos/{owner}/{repo}/pulls/7")
53
- if [ -f "$fx/head" ]; then h=$(cat "$fx/head"); else h=$(git rev-parse HEAD); fi
54
- b=
55
- [ -f "$fx/pr-body" ] && b=$(cat "$fx/pr-body")
56
- jq -n --arg h "$h" --arg b "$b" '{number:7,head:{ref:"feat/x",sha:$h},base:{ref:"main"},mergeable_state:"clean",body:$b}'
57
- ;;
58
- "api repos/{owner}/{repo}") echo "{\"full_name\":\"o/r\"}" ;;
59
- api*)
60
- printf '%s' "${@: -1}" >"$FAKE_GH_API_LOG"
61
- echo "{}"
62
- ;;
63
- *) exit 1 ;;
64
- esac
@@ -1,107 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <title>Busy</title>
6
- <!--
7
- A hero canvas that redraws every frame and spends busy=<ms> of main
8
- thread work on each, so a frame takes about that long to render. A card
9
- sits about 2900px down, below the window, and the rail link scrolls to it.
10
- With timed=1 each frame spins on performance.now() instead, as the
11
- groundwork page did, which never ends once the stepper pauses virtual
12
- time.
13
- -->
14
- <style>
15
- html {
16
- scroll-behavior: smooth;
17
- }
18
-
19
- body {
20
- margin: 0;
21
- font-family: sans-serif;
22
- }
23
-
24
- nav {
25
- position: fixed;
26
- top: 10px;
27
- right: 10px;
28
- z-index: 2;
29
- }
30
-
31
- nav a {
32
- display: block;
33
- padding: 12px 20px;
34
- background: #fff;
35
- }
36
-
37
- canvas {
38
- display: block;
39
- width: 100vw;
40
- height: 100vh;
41
- }
42
-
43
- section {
44
- height: 900px;
45
- border-top: 1px solid #ccc;
46
- padding: 40px;
47
- }
48
-
49
- .card {
50
- width: 400px;
51
- height: 240px;
52
- background: #ddd;
53
- }
54
-
55
- .card:hover {
56
- background: #f60;
57
- }
58
- </style>
59
- </head>
60
- <body>
61
- <nav><a id="rail" href="#cards">Cards</a></nav>
62
- <canvas id="hero"></canvas>
63
- <section>one</section>
64
- <section>two</section>
65
- <section id="cards"><div class="card" id="card">card</div></section>
66
- <section>four</section>
67
- <script>
68
- const params = new URLSearchParams(location.search)
69
- const busy = Number(params.get('busy') || 0)
70
- const timed = params.get('timed') === '1'
71
- const hero = document.getElementById('hero')
72
- hero.width = innerWidth
73
- hero.height = innerHeight
74
- const context = hero.getContext('2d')
75
- // The work is a count calibrated in real time on load, since a loop
76
- // timed by performance.now() never ends under a paused virtual clock.
77
- let sink = 0
78
- const spin = (count) => {
79
- for (let i = 0; i < count; i += 1) sink += Math.sqrt(i)
80
- }
81
- const measure = (ms) => {
82
- let count = 0
83
- const start = performance.now()
84
- while (performance.now() - start < ms) {
85
- spin(10000)
86
- count += 10000
87
- }
88
- return count / ms
89
- }
90
- // Warmed first, so the count matches the optimized loop a frame runs.
91
- measure(100)
92
- const work = Math.round(busy * measure(200))
93
- const draw = (time) => {
94
- if (timed) {
95
- const start = performance.now()
96
- while (performance.now() - start < busy) {}
97
- } else {
98
- spin(work)
99
- }
100
- context.fillStyle = `hsl(${(time / 10) % 360}, 60%, 50%)`
101
- context.fillRect(0, 0, hero.width, hero.height)
102
- requestAnimationFrame(draw)
103
- }
104
- requestAnimationFrame(draw)
105
- </script>
106
- </body>
107
- </html>
@@ -1,49 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <title>Fade</title>
6
- <!--
7
- A band that fades from black to white over exactly one second of page
8
- time once the button is pressed, so its brightness frame by frame reads
9
- how much page time each frame of a take advanced.
10
- -->
11
- <style>
12
- body {
13
- margin: 0;
14
- background: #808080;
15
- }
16
-
17
- #go {
18
- position: absolute;
19
- left: 100px;
20
- top: 100px;
21
- width: 200px;
22
- height: 60px;
23
- }
24
-
25
- #band {
26
- position: absolute;
27
- left: 0;
28
- top: 300px;
29
- width: 1920px;
30
- height: 780px;
31
- background-color: #000;
32
- transition: background-color 1000ms linear;
33
- }
34
-
35
- #band.lit {
36
- background-color: #fff;
37
- }
38
- </style>
39
- </head>
40
- <body>
41
- <button id="go">Go</button>
42
- <div id="band"></div>
43
- <script>
44
- document.getElementById('go').addEventListener('click', () => {
45
- document.getElementById('band').classList.add('lit')
46
- })
47
- </script>
48
- </body>
49
- </html>
@@ -1,38 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <title>Fetch</title>
6
- <!--
7
- A button whose press starts a fetch the server answers slowly, the way
8
- a page switch in an app loads what the next view shows. A take must keep
9
- stepping frames while the fetch is in flight.
10
- -->
11
- <style>
12
- body {
13
- margin: 0;
14
- background: #808080;
15
- }
16
-
17
- #go {
18
- position: absolute;
19
- left: 100px;
20
- top: 100px;
21
- width: 200px;
22
- height: 60px;
23
- }
24
- </style>
25
- </head>
26
- <body>
27
- <button id="go">Load</button>
28
- <p id="status">idle</p>
29
- <script>
30
- document.getElementById('go').addEventListener('click', () => {
31
- document.getElementById('status').textContent = 'loading'
32
- fetch('/slow').then(() => {
33
- document.getElementById('status').textContent = 'loaded'
34
- })
35
- })
36
- </script>
37
- </body>
38
- </html>
@@ -1,138 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <title>Input</title>
6
- <!--
7
- One target per input a take performs, each posting what the page saw to
8
- /report with a sequence number, so a test reads the inputs in the order
9
- the page received them after the take has closed the browser.
10
-
11
- - #handle follows a pointer-event drag and reports where it dropped and
12
- how many of its moves carried the held button
13
- - #frame reports each dblclick
14
- - #layer reports pointerdown, pointerup, and click with the Shift flag
15
- - #field takes focus for key steps, and every keyup reports its value
16
- -->
17
- <style>
18
- body {
19
- margin: 0;
20
- background: #f4f4f4;
21
- font: 24px sans-serif;
22
- }
23
-
24
- #handle {
25
- position: absolute;
26
- left: 200px;
27
- top: 200px;
28
- width: 80px;
29
- height: 80px;
30
- /* Neutral, so the reel's blue ring reads over it at any alpha. */
31
- background: #444;
32
- touch-action: none;
33
- }
34
-
35
- #lane {
36
- position: absolute;
37
- left: 700px;
38
- top: 160px;
39
- width: 300px;
40
- height: 160px;
41
- border: 2px dashed #555;
42
- box-sizing: border-box;
43
- }
44
-
45
- #frame,
46
- #layer {
47
- position: absolute;
48
- top: 500px;
49
- width: 200px;
50
- height: 100px;
51
- background: #ddd;
52
- user-select: none;
53
- }
54
-
55
- #frame {
56
- left: 200px;
57
- }
58
-
59
- #layer {
60
- left: 700px;
61
- }
62
-
63
- #field {
64
- position: absolute;
65
- left: 200px;
66
- top: 750px;
67
- width: 400px;
68
- height: 48px;
69
- font: inherit;
70
- }
71
- </style>
72
- </head>
73
- <body>
74
- <div id="lane"></div>
75
- <div id="handle"></div>
76
- <div id="frame">Frame</div>
77
- <div id="layer">Layer</div>
78
- <input id="field" />
79
- <script>
80
- let seq = 0
81
- const report = (entry) =>
82
- fetch('/report', {
83
- method: 'POST',
84
- body: JSON.stringify({ seq: (seq += 1), ...entry }),
85
- keepalive: true,
86
- })
87
-
88
- const handle = document.getElementById('handle')
89
- const lane = document.getElementById('lane')
90
- let drag = null
91
- handle.addEventListener('pointerdown', (event) => {
92
- handle.setPointerCapture(event.pointerId)
93
- drag = { held: 0, hover: 0 }
94
- })
95
- handle.addEventListener('pointermove', (event) => {
96
- if (!drag) return
97
- if (event.buttons === 1) drag.held += 1
98
- else drag.hover += 1
99
- handle.style.left = `${event.clientX - 40}px`
100
- handle.style.top = `${event.clientY - 40}px`
101
- })
102
- handle.addEventListener('pointerup', (event) => {
103
- if (!drag) return
104
- const box = lane.getBoundingClientRect()
105
- const onTarget =
106
- event.clientX >= box.left &&
107
- event.clientX <= box.right &&
108
- event.clientY >= box.top &&
109
- event.clientY <= box.bottom
110
- report({ type: 'drop', ...drag, onTarget, altKey: event.altKey })
111
- drag = null
112
- })
113
-
114
- document
115
- .getElementById('frame')
116
- .addEventListener('dblclick', () => report({ type: 'dblclick' }))
117
-
118
- const layer = document.getElementById('layer')
119
- for (const type of ['pointerdown', 'pointerup', 'click']) {
120
- layer.addEventListener(type, (event) =>
121
- report({ type, shiftKey: event.shiftKey }),
122
- )
123
- }
124
-
125
- const field = document.getElementById('field')
126
- document.addEventListener('keydown', (event) =>
127
- report({
128
- type: 'keydown',
129
- key: event.key,
130
- ctrlKey: event.ctrlKey,
131
- }),
132
- )
133
- document.addEventListener('keyup', (event) =>
134
- report({ type: 'keyup', key: event.key, value: field.value }),
135
- )
136
- </script>
137
- </body>
138
- </html>
@@ -1,45 +0,0 @@
1
- # Vendored zoom notice
2
-
3
- The files in this folder carry the zoom timing of [OpenScreen](https://github.com/siddharthvaddem/openscreen) at commit `ca8104c279da0d4b4481ec992ff103949e22c772`, under the MIT license below.
4
-
5
- ## What came from where
6
-
7
- | File | Source in OpenScreen, under `src/components/video-editor/videoPlayback/` |
8
- | ----------------- | -------------------------------------------------------------------------------------------------------- |
9
- | `math-utils.ts` | `mathUtils.ts` |
10
- | `region-utils.ts` | `zoomRegionUtils.ts`, with `TRANSITION_WINDOW_MS` and `ZOOM_IN_TRANSITION_WINDOW_MS` from `constants.ts` |
11
- | `spring.ts` | `motionSmoothing.ts` and `zoomSpring.ts` |
12
-
13
- ## What changed
14
-
15
- - The file names are kebab-case, as this repository names every TypeScript file, and the code is reformatted to its style
16
- - `math-utils.ts` keeps `clamp01`, `cubicBezier`, and `easeOutScreenStudio`, and drops the easings nothing here calls
17
- - `region-utils.ts` keeps `computeRegionStrength`, the connected-pan easing, and the window and gap constants. It drops the dominant-region search, its cache, the 3D rotation, and the cursor-follow focus, since the camera here aims at clicks and holds its focus by its own rule.
18
- - The zoom-out window is 700 ms rather than 1015.05 ms, and the connected pan runs 600 ms rather than 1000 ms, both on the operator's eye in groundwork track `193-ui-record`. <!-- canon-keep-retired --> The zoom-in window keeps OpenScreen's 1522.575 ms.
19
- - `spring.ts` keeps the zoom spring's stiffness 320, damping 40, mass 0.92, its rest thresholds, the delta clamp, and the overshoot clamp from `stepAxis`. OpenScreen steps that spring through the `motion` package, and this solves it in closed form instead, so no dependency comes with it.
20
-
21
- ## License
22
-
23
- ```plaintext
24
- MIT License
25
-
26
- Copyright (c) 2025 Siddharth Vaddem
27
-
28
- Permission is hereby granted, free of charge, to any person obtaining a copy
29
- of this software and associated documentation files (the "Software"), to deal
30
- in the Software without restriction, including without limitation the rights
31
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
32
- copies of the Software, and to permit persons to whom the Software is
33
- furnished to do so, subject to the following conditions:
34
-
35
- The above copyright notice and this permission notice shall be included in all
36
- copies or substantial portions of the Software.
37
-
38
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
39
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
40
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
41
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
42
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
43
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
44
- SOFTWARE.
45
- ```