@ape-egg/codecane 0.0.0-stage → 1.1.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.
@@ -0,0 +1,105 @@
1
+ <story-panel ontransitionend="event.target === this && codecane.returned()">
2
+ <panel-tabs stylecheat dark>
3
+ <tabs primary>
4
+ <tab active="@[tab === 'about']" onclick="$.tab = 'about'">About</tab>
5
+ <tab active="@[tab === 'files']" onclick="$.tab = 'files'">Files</tab>
6
+ </tabs>
7
+ </panel-tabs>
8
+ <!-- if tab === 'about' -->
9
+ <article>
10
+ <step-status stylecheat dark>
11
+ <crow justify-between>
12
+ <small>Step @[timeline.indexOf(page.id) + 1] of @[timeline.length]</small>
13
+ <!-- if page.untested -->
14
+ <badge bg-slate text-slate>Untested</badge>
15
+ <!-- /if -->
16
+ </crow>
17
+ </step-status>
18
+ <!-- if page.convergence && page.convergence.kind === 'seam' -->
19
+ <story><h1>@[page.title]</h1></story>
20
+ <!-- else -->
21
+ <story>@[$.unsafe(page.story)]</story>
22
+ <!-- /if -->
23
+ <!-- if page.convergence -->
24
+ <convergence kind="@[page.convergence.kind]">
25
+ <h2>@[kinds[page.convergence.kind]]</h2>
26
+ <!-- if page.convergence.kind === 'seam' -->
27
+ <p>A seam is a small API that the timelines before it share. Each timeline fills it in where it owns the code, so the steps after it are written once and never ask which timeline they are on.</p>
28
+ <!-- if page.convergence.requiredBy.length -->
29
+ <p>Required by @[page.convergence.requiredBy.map(id => steps[id].title).join(', ')]:</p>
30
+ <ul>
31
+ <!-- each page.convergence.reasons as reason -->
32
+ <li>@[reason]</li>
33
+ <!-- /each -->
34
+ </ul>
35
+ <!-- /if -->
36
+ <!-- else -->
37
+ <!-- if page.convergence.continues.length -->
38
+ <p>Continues @[page.convergence.continues.map(id => steps[id].title).join(', ')] together, each timeline on its own.</p>
39
+ <!-- /if -->
40
+ <!-- if page.convergence.brought.length -->
41
+ <p>Brings @[page.convergence.brought.map(id => steps[id].title).join(', ')] onto this timeline.</p>
42
+ <!-- /if -->
43
+ <!-- if page.convergence.kind === 'merge' -->
44
+ <p>Every step replays cleanly and every check passes, so the timelines just join.</p>
45
+ <!-- /if -->
46
+ <!-- /if -->
47
+ </convergence>
48
+ <!-- /if -->
49
+ <!-- if page.convergence && page.convergence.kind === 'seam' -->
50
+ <story>@[$.unsafe(page.story)]</story>
51
+ <!-- /if -->
52
+ <!-- if alternatives.length -->
53
+ <alternatives>
54
+ <h2>Alternatives</h2>
55
+ <!-- each alternatives as alternative -->
56
+ <a href="/@[alternative.address]">@[alternative.title]</a>
57
+ <!-- /each -->
58
+ </alternatives>
59
+ <!-- /if -->
60
+ </article>
61
+ <!-- else -->
62
+ <file-list stylecheat dark>
63
+ <!-- each [...new Set(page.files.map(file => file.folder))] as folder (page.id + folder) -->
64
+ <!-- if folder -->
65
+ <folder>@[folder]</folder>
66
+ <!-- /if -->
67
+ <!-- each page.files.filter(file => file.folder === folder) as file (page.id + file.path) -->
68
+ <file data-path="@[file.path]" aria-current="@[file.path === open ? 'location' : 'false']" onclick="codecane.open('@[file.path]')" onmouseenter="codecane.light('@[file.changedBy]')" onmouseleave="codecane.light()">
69
+ <span>@[file.path.slice(folder.length)]</span>
70
+ <!-- if file.owner === page.id && file.overrides -->
71
+ <badge bg-orange text-orange>Override</badge>
72
+ <!-- /if -->
73
+ <!-- if file.owner === page.id && !file.overrides -->
74
+ <badge bg-green text-green>Added</badge>
75
+ <!-- /if -->
76
+ <!-- if file.patchedBy.includes(page.id) -->
77
+ <badge bg-yellow text-yellow>Modified</badge>
78
+ <!-- /if -->
79
+ </file>
80
+ <!-- /each -->
81
+ <!-- /each -->
82
+ <!-- if page.dependencies.length -->
83
+ <folder>node_modules/</folder>
84
+ <!-- each page.dependencies as dependency (page.id + dependency) -->
85
+ <dependency>@[dependency]</dependency>
86
+ <!-- /each -->
87
+ <!-- /if -->
88
+ </file-list>
89
+ <!-- /if -->
90
+ <footer>
91
+ <!-- if page.ahead -->
92
+ <story>@[$.unsafe(page.ahead)]</story>
93
+ <!-- /if -->
94
+ <step-links>
95
+ <!-- if previous -->
96
+ <a href="/@[previous.address]" kind="@[previous.kind]">← @[previous.title]</a>
97
+ <!-- /if -->
98
+ <!-- if next -->
99
+ <a href="/@[next.address]" next kind="@[next.kind]">@[next.title] →</a>
100
+ <!-- else -->
101
+ <span>Final build</span>
102
+ <!-- /if -->
103
+ </step-links>
104
+ </footer>
105
+ </story-panel>
@@ -0,0 +1,25 @@
1
+ <header @[fold] stylecheat dark away="@[fullscreen && !returning]">
2
+ <a href="/" aria-current="@[root ? 'page' : 'false']"><strong>@[name]</strong></a>
3
+ <ol style="grid-template-rows: @[rows]">
4
+ <!-- each cells as cell (cell.key) -->
5
+ <li style="grid-area: @[cell.lane + 1] / @[cell.depth + 1] / span @[cell.span]" @[cell.shown]>
6
+ <!-- if cell.follows -->
7
+ <arrow>→</arrow>
8
+ <!-- /if -->
9
+ <a href="/@[cell.address]" onclick="codecane.pick(event, '@[cell.id]', @[cell.lane], @[cell.span])" aria-current="@[page.id === cell.id ? 'step' : 'false']" kind="@[steps[cell.id].kind ?? '']" @[cell.lit]>
10
+ <!-- if steps[cell.id].kind -->
11
+ <join aria-hidden="true">Y</join>
12
+ <!-- /if -->
13
+ <!-- if steps[cell.id].kind !== 'seam' -->
14
+ @[steps[cell.id].title]
15
+ <!-- /if -->
16
+ </a>
17
+ <!-- if cell.forks -->
18
+ <button fork title="@[cell.forks] timelines fork after @[steps[cell.id].title]" aria-expanded="@[fold && through === cell.id ? 'true' : 'false']" onclick="codecane.fold('@[cell.id]')"><split aria-hidden="true">Y</split>@[cell.forks]</button>
19
+ <!-- /if -->
20
+ </li>
21
+ <!-- /each -->
22
+ </ol>
23
+ <logotext-codecane><a href="https://codecane.korte.kim" target="_blank">code<span>cane</span></a><version>v@[version]</version></logotext-codecane>
24
+ <button onclick="codecane.expand()" aria-label="Fullscreen" tertiary sm><icon fullscreen></icon></button>
25
+ </header>
package/ui/index.html ADDED
@@ -0,0 +1,20 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>codecane</title>
7
+ <link rel="stylesheet" href="/node_modules/@ape-egg/codie/codie.css">
8
+ <link rel="stylesheet" href="/codecane/ui/stylecheat.css">
9
+ <link rel="stylesheet" href="/codecane/ui/codecane.css">
10
+ <script type="module" src="/codecane/ui/viewer.js"></script>
11
+ </head>
12
+ <body vibe-fouc>
13
+ <component src="/codecane/ui/components/Timeline.html"></component>
14
+ <workspace fullscreen="@[fullscreen]" away="@[fullscreen && !returning]">
15
+ <component src="/codecane/ui/components/Story.html"></component>
16
+ <component src="/codecane/ui/components/Stage.html"></component>
17
+ <component src="/codecane/ui/components/Changes.html"></component>
18
+ </workspace>
19
+ </body>
20
+ </html>
package/ui/mounted.js ADDED
@@ -0,0 +1,73 @@
1
+ const blobs = {}
2
+ const sources = {}
3
+ const blob = (text, type, path) => {
4
+ const url = blobs[`${path}\n${text}`] ??= URL.createObjectURL(new Blob([text], { type }))
5
+ sources[url] = path
6
+ return url
7
+ }
8
+
9
+ const moduleLinks = /((?:from|import)\s*\(?\s*)(['"])(\.{1,2}\/[^'"]+)\2/g
10
+ const styleLinks = /((?:url\(|@import\s+(?!url\())\s*)(['"]?)([^'")\s;]+)\2/g
11
+
12
+ const reporter = `{
13
+ let failed = false
14
+ const fail = failure => {
15
+ failed = true
16
+ parent.postMessage({ codecane: 'failed', ...failure }, origin)
17
+ }
18
+ addEventListener('error', event => fail(event instanceof ErrorEvent
19
+ ? { message: String(event.error ?? event.message), source: event.filename, line: event.lineno }
20
+ : { message: 'Could not load ' + (event.target.src ?? event.target.href), source: '' }), true)
21
+ addEventListener('unhandledrejection', ({ reason }) => fail({ message: String(reason), source: '' }))
22
+ addEventListener('load', () => failed || parent.postMessage({ codecane: 'ran' }, origin))
23
+ }`
24
+
25
+ export const pathOf = (page, url) => sources[url] ?? (url.startsWith(page.base) ? url.split(/[?#]/)[0].slice(page.base.length) : url)
26
+
27
+ const sheets = (page, drafts, text) => {
28
+ const address = path => new URL(path, page.base).href
29
+ const has = path => page.files.some(file => file.path === path)
30
+ const styles = {}
31
+ const styled = path => styles[path] ??= (async () => {
32
+ const css = drafts[path] ?? await text(path)
33
+ const targets = Object.fromEntries(await Promise.all([...css.matchAll(styleLinks)].map(async ([, , , link]) => {
34
+ const target = new URL(link, address(path)).href
35
+ return [link, pathOf(page, target).endsWith('.css') && has(pathOf(page, target)) ? await styled(pathOf(page, target)) : target]
36
+ })))
37
+ return blob(css.replace(styleLinks, (_, lead, quote, link) => `${lead}${quote}${targets[link]}${quote}`), 'text/css', path)
38
+ })()
39
+ return { address, has, styled }
40
+ }
41
+
42
+ export const restyled = (page, drafts, text) => {
43
+ const { has, styled } = sheets(page, drafts, text)
44
+ return async href => has(pathOf(page, href)) ? styled(pathOf(page, href)) : href
45
+ }
46
+
47
+ export const mounted = async (page, drafts, text) => {
48
+ const { address, has, styled } = sheets(page, drafts, text)
49
+
50
+ const modules = Object.fromEntries(Object.keys(drafts).filter(path => path.endsWith('.js')).map(path => [
51
+ address(path),
52
+ blob(drafts[path].replace(moduleLinks, (_, lead, quote, link) => `${lead}${quote}${new URL(link, address(path))}${quote}`), 'text/javascript', path),
53
+ ]))
54
+
55
+ const doc = new DOMParser().parseFromString(drafts['index.html'] ?? await text('index.html'), 'text/html')
56
+ const report = Object.assign(doc.createElement('script'), { textContent: reporter })
57
+ const base = Object.assign(doc.createElement('base'), { href: page.base })
58
+ const map = doc.querySelector('script[type="importmap"]') ?? Object.assign(doc.createElement('script'), { type: 'importmap', textContent: '{}' })
59
+ const { imports, ...rest } = JSON.parse(map.textContent)
60
+ map.textContent = JSON.stringify({ ...rest, imports: { ...imports, ...modules } })
61
+ doc.head.prepend(report, base, map)
62
+
63
+ await Promise.all([...doc.querySelectorAll('link[rel="stylesheet"][href]')].map(async link => {
64
+ const path = pathOf(page, new URL(link.getAttribute('href'), page.base).href)
65
+ if (has(path)) link.setAttribute('href', await styled(path))
66
+ }))
67
+ doc.querySelectorAll('script[type="module"][src]').forEach(script => {
68
+ const target = new URL(script.getAttribute('src'), page.base).href
69
+ script.setAttribute('src', modules[target] ?? target)
70
+ })
71
+
72
+ return blob(`<!doctype html>\n${doc.documentElement.outerHTML}`, 'text/html', 'index.html')
73
+ }