diff --git a/assets/sass/cheat-sheet.scss b/assets/sass/cheat-sheet.scss index f430400343..d0d0f741cb 100644 --- a/assets/sass/cheat-sheet.scss +++ b/assets/sass/cheat-sheet.scss @@ -45,6 +45,43 @@ background: transparent; } + code { + transition: box-shadow 0.2s ease, background-color 0.2s ease; + } + + code[role="button"] { + cursor: pointer; + + &:hover { + background: color-mix(in srgb, var(--orange) 8%, var(--callout-color)); + } + + &:focus-visible { + outline: 2px solid var(--orange); + outline-offset: 1px; + } + } + + code[aria-current="step"], + code[aria-current="step"]:hover { + background: color-mix(in srgb, var(--orange) 16%, var(--callout-color)); + box-shadow: inset 4px 0 0 var(--orange); + } + + .git-sim { + flex: 1 1 auto; + display: flex; + flex-direction: column; + margin-top: 0; + border: 1px solid var(--callout-color); + border-radius: 6px; + overflow: hidden; + + iframe { + flex: 1 0 auto; + } + } + .or { font-weight: 700; font-size: 0.8em; diff --git a/content/cheat-sheet/_index.html b/content/cheat-sheet/_index.html index a85d4d5480..8ed2f84a29 100644 --- a/content/cheat-sheet/_index.html +++ b/content/cheat-sheet/_index.html @@ -298,166 +298,14 @@

Combine with rebase:

git switch banana git rebase main - - {{< graphviz engine="neato">}} - digraph G { - - bgcolor="transparent"; - rankdir=TB; - node [ - shape=box, width=0.6, height=0.6, penwidth=3, - fixedsize=true, fontname="monospace", - fontsize=24 - ]; - edge [arrowsize=1.0, dir=none, penwidth=3]; - - - // Nodes - A [label="A", pos="0,1!"]; - B [label="B", pos="1,1!"]; - C [label="C", pos="2,1!"]; - D [label="D", color="#f14e32", pos="2,0!"]; - E [label="E", color="#f14e32", pos="3,0!"]; - - main_label [label="main", shape=plaintext, pos="5,1!", fixedsize=false]; - main_label -> C [dir=forward]; - - banana_label [label="banana", shape=plaintext, pos="5.2,0!", fixedsize=false]; - banana_label -> E [dir=forward]; - - // Edges - A -> B; - B -> C; - B -> D; - D -> E; - } - - {{< /graphviz>}} - - {{< graphviz engine="neato">}} - digraph G { - - bgcolor="transparent"; - rankdir=TB; - node [ - shape=box, width=0.6, height=0.6, penwidth=3, - fixedsize=true, fontname="monospace", - fontsize=24 - ]; - edge [arrowsize=1.0, dir=none, penwidth=3]; - - - // Nodes - A [label="A", pos="0,1!"]; - B [label="B", pos="1,1!"]; - C [label="C", pos="2,1!"]; - D [label="D", style="dashed", color="#f14e32", pos="2,0!"]; - E [label="E", style="dashed", color="#f14e32", pos="3,0!"]; - - D2 [label="D′", color="#f14e32", pos="3,1!"]; - E2 [label="E′", color="#f14e32", pos="4,1!"]; - - - main_label [label="main", shape=plaintext, pos="2,2!", fixedsize=false]; - main_label -> C [dir=forward]; - - banana_label [label="banana", shape=plaintext, pos="4,2!", fixedsize=false]; - banana_label -> E2 [dir=forward]; - - - lost_label [label="\"lost\"", shape=plaintext, pos="5.2,0!", fixedsize=false]; - lost_label -> E [dir=forward]; - - // Edges - A -> B; - B -> C; - C -> D2; - D2 -> E2; - - B -> D; - D -> E; - } - - {{< /graphviz>}} +

Combine with merge:

git switch main git merge banana - - {{< graphviz engine="neato">}} - digraph G { - - bgcolor="transparent"; - rankdir=TB; - node [ - shape=square, width=0.6, height=0.6, penwidth=3, - fixedsize=true, fontname="monospace", - fontsize=24 - ]; - edge [arrowsize=1.0, dir=none, penwidth=3]; - - - // Nodes - A [label="A", pos="0,1!"]; - B [label="B", pos="1,1!"]; - C [label="C", pos="2,1!"]; - D [label="D", color="#f14e32", pos="2,0!"]; - E [label="E", color="#f14e32", pos="3,0!"]; - - main_label [label="main", shape=plaintext, pos="5,1!", fixedsize=false]; - main_label -> C [dir=forward]; - - banana_label [label="banana", shape=plaintext, pos="5.2,0!", fixedsize=false]; - banana_label -> E [dir=forward]; - - // Edges - A -> B; - B -> C; - B -> D; - D -> E; - } - - {{< /graphviz>}} - - {{< graphviz engine="neato">}} - digraph G { - - bgcolor="transparent"; - rankdir=TB; - node [ - shape=square, width=0.6, height=0.6, penwidth=3, - fixedsize=true, fontname="monospace", - fontsize=24 - ]; - edge [arrowsize=1.0, dir=none, penwidth=3]; - - - // Nodes - A [label="A", pos="0,1!"]; - B [label="B", pos="1,1!"]; - C [label="C", pos="2,1!"]; - D [label="D", color="#f14e32", pos="2,0!"]; - E [label="E", color="#f14e32", pos="3,0!"]; - M [label="◇", pos="4,1!"]; - - main_label [label="main", shape=plaintext, pos="5,1!", fixedsize=false]; - main_label -> M [dir=forward]; - - banana_label [label="banana", shape=plaintext, pos="5.2,0!", fixedsize=false]; - banana_label -> E [dir=forward]; - - // Edges - A -> B; - B -> C; - B -> D; - D -> E; - C -> M; - E -> M; - } - - {{< /graphviz>}} +

Combine with squash merge:

@@ -465,222 +313,20 @@

Combine with squash merge:

git merge --squash banana git commit - - {{< graphviz engine="neato">}} - digraph G { - - bgcolor="transparent"; - rankdir=TB; - node [ - shape=square, width=0.6, height=0.6, penwidth=3, - fixedsize=true, fontname="monospace", - fontsize=24 - ]; - edge [arrowsize=1.0, dir=none, penwidth=3]; - - - // Nodes - A [label="A", pos="0,1!"]; - B [label="B", pos="1,1!"]; - C [label="C", pos="2,1!"]; - D [label="D", color="#f14e32", pos="2,0!"]; - E [label="E", color="#f14e32", pos="3,0!"]; - - main_label [label="main", shape=plaintext, pos="5,1!", fixedsize=false]; - main_label -> C [dir=forward]; - - banana_label [label="banana", shape=plaintext, pos="5.2,0!", fixedsize=false]; - banana_label -> E [dir=forward]; - - // Edges - A -> B; - B -> C; - B -> D; - D -> E; - } - - {{< /graphviz>}} - - {{< graphviz engine="neato">}} - digraph G { - - bgcolor="transparent"; - rankdir=TB; - node [ - shape=rect, width=0.6, height=0.6, penwidth=3, - fixedsize=true, fontname="monospace", - fontsize=24 - ]; - edge [arrowsize=1.0, dir=none, penwidth=3]; - - - // Nodes - A [label="A", pos="0,1!"]; - B [label="B", pos="1,1!"]; - C [label="C", pos="2,1!"]; - D [label="D", color="#f14e32", pos="2,0!"]; - E [label="E", color="#f14e32", pos="3,0!"]; - S [label="D\nE", color="#f14e32", pos="3,1!", width=0.6, height=0.9]; - - main_label [label="main", shape=plaintext, pos="5,1!", fixedsize=false]; - main_label -> S [dir=forward]; - - banana_label [label="banana", shape=plaintext, pos="5.2,0!", fixedsize=false]; - banana_label -> E [dir=forward]; - - // Edges - A -> B; - B -> C; - B -> D; - D -> E; - C -> S; - } - - {{< /graphviz>}} +

Bring a branch up to date with another branch (aka "fast-forward merge"):

git switch main git merge banana - - {{< graphviz engine="neato">}} - digraph G { - - bgcolor="transparent"; - rankdir=LR; - node [ - shape=rect, width=0.6, height=0.6, penwidth=3, - fixedsize=true, fontname="monospace", - fontsize=24 - ]; - edge [arrowsize=1.0, dir=none, penwidth=3]; - - - // Nodes - A [label="A", pos="0,0!"]; - B [label="B", pos="1,0!"]; - C [label="C", pos="2,0!"]; - D [label="D", pos="3,0!"]; - E [label="E", pos="4,0!"]; - invisible [label="", shape=plaintext,pos="6,0!"]; - - main_label [label="main", fontcolor="#f14e32", shape=plaintext, pos="2,-1!", fixedsize=false]; - main_label -> C [dir=forward]; - - banana_label [label="banana", shape=plaintext, pos="4,-1!", fixedsize=false]; - banana_label -> E [dir=forward]; - - // Edges - A -> B; - B -> C; - C -> D; - D -> E; - } - {{< /graphviz>}} - - {{< graphviz engine="neato">}} - digraph G { - - bgcolor="transparent"; - rankdir=LR; - node [ - shape=rect, width=0.6, height=0.6, penwidth=3, - fixedsize=true, fontname="monospace", - fontsize=24 - ]; - edge [arrowsize=1.0, dir=none, penwidth=3]; - - - // Nodes - A [label="A", pos="0,0!"]; - B [label="B", pos="1,0!"]; - C [label="C", pos="2,0!"]; - D [label="D", pos="3,0!"]; - E [label="E", pos="4,0!"]; - - main_label [label="main", fontcolor="#f14e32", shape=plaintext, pos="6, 0!", fixedsize=false]; - main_label -> E [dir=forward]; - - banana_label [label="banana", shape=plaintext, pos="4,-1!", fixedsize=false]; - banana_label -> E [dir=forward]; - - // Edges - A -> B; - B -> C; - C -> D; - D -> E; - } - {{< /graphviz>}} +

Copy one commit onto the current branch:

git cherry-pick <commit> - - - {{< graphviz engine="neato">}} - digraph G { - bgcolor="transparent"; - rankdir=TB; - node [ - shape=box, width=0.6, height=0.6, penwidth=3, - fixedsize=true, fontname="monospace", - fontsize=24 - ]; - edge [arrowsize=1.0, dir=none, penwidth=3]; - - - // Nodes - A [label="A", pos="0,1!"]; - B [label="B", pos="1,1!"]; - C [label="C", pos="2,1!"]; - D [label="D", color="#f14e32", pos="2,0!"]; - E [label="E", pos="3,0!"]; - - main_label [label="main", shape=plaintext, pos="5,1!", fixedsize=false]; - main_label -> C [dir=forward]; - - // Edges - A -> B; - B -> C; - B -> D; - D -> E; - } - {{< /graphviz>}} - - - {{< graphviz engine="neato">}} - digraph G { - bgcolor="transparent"; - rankdir=TB; - node [ - shape=box, width=0.6, height=0.6, penwidth=3, - fixedsize=true, fontname="monospace", - fontsize=24 - ]; - edge [arrowsize=1.0, dir=none, penwidth=3]; - - - // Nodes - A [label="A", pos="0,1!"]; - B [label="B", pos="1,1!"]; - C [label="C", pos="2,1!"]; - D2 [label="D′", color="#f14e32", pos="3,1!"]; - - D [label="D", color="#f14e32", pos="2,0!"]; - E [label="E", pos="3,0!"]; - - main_label [label="main", shape=plaintext, pos="5,1!", fixedsize=false]; - main_label -> D2 [dir=forward]; - // Edges - A -> B; - B -> C; - C -> D2; - B -> D; - D -> E; - } - {{< /graphviz>}} +
@@ -778,4 +424,5 @@

List of files to ignore:

.gitignore + diff --git a/static/git-sim/assets/git-sim-session.css b/static/git-sim/assets/git-sim-session.css new file mode 100644 index 0000000000..0998b0f962 --- /dev/null +++ b/static/git-sim/assets/git-sim-session.css @@ -0,0 +1,247 @@ +/* Generated by git-sim 0.4.x (https://github.com/initialcommit-com/git-sim) + from src/git_sim/render/html.py and live_html.py. Do not edit: regenerate it with git-sim. */ +:root{--bg:#2A2A2A;--text:#B3B1B1;--muted:#9A9994;--rule:#5E5E5A;--accent:#D7834F;--panel:rgba(0,0,0,.05);--font:"Cascadia Mono","JetBrains Mono","Fira Mono","SF Mono",Menlo,Consolas,"DejaVu Sans Mono","Liberation Mono",monospace;} +:root[data-theme="light"]{--bg:#F0EFE7;--text:#000000;--muted:#4E443C;--rule:#D8D7CF;--accent:#F14E32;--panel:rgba(0,0,0,.05);} +*{box-sizing:border-box} +html,body{margin:0;min-height:100%;background:var(--bg);color:var(--text);font-family:var(--font)} +#bar{position:sticky;top:var(--bar-top,0);z-index:2;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:12px;height:64px;padding:0 18px;background:var(--bg);border-bottom:1px solid var(--rule)} +#bar .right{display:flex;justify-content:flex-end;align-items:center;gap:6px;color:var(--muted);font:12px/1 var(--font);position:relative} +#bar .right a{color:var(--muted);text-decoration:none;font:600 12px/1 var(--font);padding:8px 10px;border-radius:999px;border:1px solid transparent} +#bar .right a:hover{color:var(--text);border-color:var(--rule);background:var(--panel)} +#help{width:34px;height:34px;border-radius:50%;border:1px solid var(--rule);background:var(--panel);color:var(--muted);font:700 14px/1 var(--font);cursor:pointer;margin-left:6px} +#help:hover,#help.on{color:var(--text);border-color:var(--text)} +#helpMenu,#shareMenu{position:absolute;top:46px;right:0;width:360px;background:var(--bg);border:1px solid var(--rule);border-radius:12px;padding:12px 14px;box-shadow:0 12px 36px rgba(0,0,0,.4);z-index:4;text-align:left} +#shareMenu{right:44px;width:340px} +#helpMenu[hidden],#shareMenu[hidden]{display:none} +#helpMenu h3,#shareMenu h3{margin:0 0 8px;font:700 12px/1 var(--font);color:var(--muted);letter-spacing:.06em;text-transform:uppercase} +#helpMenu ul{list-style:none;margin:0;padding:0} +#helpMenu li{display:flex;gap:10px;padding:5px 0;font:13px/1.4 var(--font);color:var(--text)} +#helpMenu kbd{flex:0 0 108px;padding:0;border:0;border-radius:0;background:none;box-shadow:none;color:var(--accent);font:700 12px/1.4 var(--font)} +#share{border:1px solid var(--accent);background:transparent;color:var(--accent);font:700 12px/1 var(--font);padding:8px 14px;border-radius:999px;cursor:pointer;margin-left:6px} +#share:hover,#share.on{background:var(--accent);color:var(--bg)} +#shareMenu .grid{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:12px} +#shareMenu .grid button,#shareMenu .grid a{display:block;text-align:center;border:1px solid var(--rule);background:var(--panel);color:var(--text);text-decoration:none;font:600 12px/1 var(--font);padding:9px 10px;border-radius:8px;cursor:pointer} +#shareMenu .grid button:hover,#shareMenu .grid a:hover{border-color:var(--accent);color:var(--accent)} +#localNote{display:none;margin:0;color:var(--muted);font:12px/1.45 var(--font)} +#publicPanel{margin:-4px 0 12px;padding:10px 12px;border:1px solid var(--rule);border-radius:10px;background:var(--panel)} +#publicPanel[hidden],#publicPanel [hidden]{display:none} +#publicPanel p{margin:0 0 10px;color:var(--muted);font:12px/1.45 var(--font)} +.pub-actions{display:flex;gap:6px} +.pub-actions button{flex:1;border:1px solid var(--rule);background:var(--bg);color:var(--text);font:600 12px/1.25 var(--font);padding:9px 10px;border-radius:8px;cursor:pointer} +.pub-actions #publicGo{flex:2;border-color:var(--accent);background:var(--accent);color:var(--bg)} +.pub-actions button:hover{border-color:var(--accent)} +.pub-actions button:disabled{opacity:.6;cursor:default} +#publicPanel .pub-alt{margin:10px 0 0} +#publicPanel .pub-alt a{margin:0;padding:0;font:inherit;color:var(--accent);text-decoration:underline} +#publicPanel .pub-head{display:flex;align-items:center;gap:7px;margin:0 0 12px;color:var(--text);font:600 12.5px/1.3 var(--font)} +#publicPanel .pub-head svg{flex:none;color:#16a34a} +.pub-row{margin:0 0 12px} +.pub-row:last-child{margin-bottom:0} +.pub-label{display:block;margin:0 0 5px;color:var(--muted);font:700 10.5px/1 var(--font);letter-spacing:.07em;text-transform:uppercase} +.pub-field{display:flex;align-items:stretch;border:1px solid var(--rule);border-radius:7px;background:var(--bg);overflow:hidden;transition:border-color .15s} +.pub-field:focus-within{border-color:var(--accent)} +.pub-value{flex:1;min-width:0;padding:7px 9px;color:var(--text);font:11.5px/1.45 var(--font);overflow-wrap:anywhere;word-break:break-all;-webkit-user-select:all;user-select:all;cursor:text;outline:none} +.pub-copy{flex:none;display:inline-flex;align-items:center;justify-content:center;width:34px;padding:0;border:0;border-left:1px solid var(--rule);background:transparent;color:var(--muted);cursor:pointer;transition:color .15s,background .15s} +.pub-copy:hover{color:var(--accent);background:var(--panel)} +.pub-copy .ok,.pub-copy.done .cp{display:none} +.pub-copy.done .ok{display:block} +.pub-copy.done{color:#16a34a} +.pub-row small{display:block;margin-top:5px;color:var(--muted);font:11.5px/1.4 var(--font)} +#toast{position:fixed;left:50%;bottom:26px;transform:translate(-50%,12px);opacity:0;transition:opacity .25s,transform .25s;background:var(--text);color:var(--bg);padding:9px 16px;border-radius:999px;font:600 13px/1 var(--font);z-index:5;pointer-events:none} +#toast.show{opacity:1;transform:translate(-50%,0)} +#brand{color:var(--muted);text-decoration:none;font:600 13px/1 var(--font);letter-spacing:.04em} +#brand:hover{color:var(--text)} +#controls{display:flex;align-items:center;gap:14px;padding:6px 10px 6px 6px;border:1px solid var(--rule);border-radius:999px;background:var(--panel)} +#controls.off{visibility:hidden} +#controls.locked{opacity:.45;cursor:not-allowed} +#controls.locked button,#controls.locked input{pointer-events:none} +#play{width:40px;height:40px;border-radius:50%;border:0;background:var(--accent);color:var(--bg);font:700 15px/1 var(--font);cursor:pointer;display:grid;place-items:center;box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 25%,transparent)} +#play:hover{filter:brightness(1.08)} +.end{border:0;background:transparent;color:var(--muted);font:700 15px/1 var(--font);padding:6px 4px;cursor:pointer;letter-spacing:.02em} +.end.on{color:var(--text)} +#scrub{-webkit-appearance:none;appearance:none;width:min(44vw,520px);height:6px;margin:0 4px;border-radius:999px;outline:none;cursor:pointer; + background:linear-gradient(90deg,var(--accent) var(--pct,100%),var(--rule) var(--pct,100%))} +#scrub::-webkit-slider-thumb{-webkit-appearance:none;width:22px;height:22px;border-radius:50%;background:var(--accent);border:3px solid var(--bg);box-shadow:0 0 0 2px var(--accent);cursor:grab} +#scrub::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background:var(--accent);border:3px solid var(--bg);box-shadow:0 0 0 2px var(--accent);cursor:grab} +#scrub:active::-webkit-slider-thumb{cursor:grabbing} +#stepLabel{color:var(--muted);font:600 12px/1 var(--font);min-width:84px;text-align:left} +#stepLabel[hidden]{display:none} +#bar .right a#learn{display:inline-flex;align-items:center;gap:7px;padding:7px 13px 7px 10px;border-color:var(--rule);background:var(--panel);color:var(--text);font:700 12px/1 var(--font);white-space:nowrap} +#bar .right a#learn svg{flex:none;color:var(--accent)} +#bar .right a#learn:hover{border-color:var(--accent);color:var(--accent)} +#bar .right a#learn[hidden],#learn[hidden]{display:none} +#helpMenu .help-more{margin:12px 0 0;padding-top:12px;border-top:1px solid var(--rule)} +#helpMenu .help-more a{display:flex;align-items:center;gap:10px;padding:10px 12px;border:1px solid var(--rule);border-radius:10px;background:var(--panel);color:var(--text);text-decoration:none;transition:border-color .15s} +#helpMenu .help-more a:hover{border-color:var(--accent)} +#helpMenu .help-more .more-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px} +#helpMenu .help-more b{font:700 13px/1.3 var(--font)} +#helpMenu .help-more small{color:var(--muted);font:600 10.5px/1.3 var(--font);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +#helpMenu .help-more .more-go{flex:none;color:var(--accent);font:700 14px/1 var(--font)} +#bar.no-brand{grid-template-columns:1fr auto} +#helpMenu .help-credit{margin:12px 0 0;padding-top:12px;border-top:1px solid var(--rule);font:13px/1.5 var(--font);color:var(--muted);text-align:right} +#helpMenu .help-credit a{display:inline-block;margin-left:4px;padding:2px 10px;border-radius:999px;border:1px solid #c7d2fe;background:#eef2ff;color:#3730a3;font-weight:700;text-decoration:none} +#helpMenu .help-credit a:hover{background:#4f46e5;border-color:#4f46e5;color:#fff} +[data-theme="dark"] #helpMenu .help-credit a{border-color:rgba(129,140,248,.38);background:rgba(99,102,241,.16);color:#c7d2fe} +[data-theme="dark"] #helpMenu .help-credit a:hover{background:#818cf8;border-color:#818cf8;color:#fff} +@media (max-width:1180px){#scrub{width:min(36vw,520px)}} +@media (max-width:640px){#learn{display:none}} +@media (max-width:820px){#bar{grid-template-columns:auto 1fr auto;padding:0 10px}#brand,#stepLabel{display:none}#scrub{width:min(40vw,520px)}} +@media (max-width:480px){#bar{grid-template-columns:minmax(0,1fr) auto;gap:6px;padding:0 6px}#controls{min-width:0;gap:6px;padding:4px 8px 4px 4px}#play{flex:none;width:32px;height:32px;font-size:13px}.end{flex:none;font-size:12.5px;padding:4px 2px}#scrub{flex:1 1 auto;width:auto;min-width:40px;margin:0}#help,#helpMenu{display:none!important}#share{margin-left:0;padding:7px 10px}#shareMenu{right:0;width:min(340px,calc(100vw - 16px))}} +#stage{padding:0 0 24px} +#scene{display:block;width:100%;height:auto;margin:0 auto;user-select:none} +#scene .dim{filter:opacity(.18)} +#scene .lit{opacity:1 !important} +#scene [data-role="commit"],#scene [data-role="commit-label"],#scene [data-role="commit-hit"],#scene [data-role="ref"]{cursor:pointer} +#tip{position:fixed;pointer-events:none;display:none;max-width:420px;background:var(--bg);color:var(--text);border:1px solid var(--rule);border-radius:10px;padding:10px 12px;font:13px/1.45 var(--font);box-shadow:0 8px 28px rgba(0,0,0,.35);z-index:3} +#tip .k{color:var(--muted)} +#tip .sha{color:var(--accent);font-weight:700} +#tip .msg{margin:4px 0 6px;white-space:pre-wrap} +#viewerNote{max-width:720px;margin:48px auto;padding:0 18px;color:var(--muted);font:14px/1.6 var(--font);text-align:center} +#viewerNote b{color:var(--text)} +#openedNote{max-width:960px;margin:14px auto 0;padding:10px 14px;display:flex;gap:12px;align-items:center;border:1px solid var(--rule);border-radius:10px;background:var(--panel);color:var(--muted);font:13px/1.5 var(--font)} +#openedNote b{color:var(--text)} +#openedNote code{font:12px/1.4 var(--font);color:var(--text);background:var(--bg);border:1px solid var(--rule);border-radius:5px;padding:1px 5px;word-break:break-all} +#openedNote button{margin-left:auto;flex:none;border:1px solid var(--rule);background:transparent;color:var(--muted);border-radius:6px;width:26px;height:26px;cursor:pointer;font:600 13px/1 var(--font)} +#openedNote button:hover{color:var(--accent);border-color:var(--accent)} + +#live{position:sticky;top:var(--live-top,64px);z-index:2;display:flex;align-items:center;gap:10px;padding:8px 14px;background:var(--bg);border-bottom:1px solid var(--rule);font:12px/1 var(--font);color:var(--muted)} +#liveDot{flex:none;width:9px;height:9px;border-radius:50%;background:var(--muted);box-shadow:0 0 0 0 transparent} +#liveDot.on{background:#22c55e;animation:pulse 1.6s ease-out infinite} +#liveDot.off{background:#ef4444} +#liveDot.rec{background:#ef4444;animation:pulse-red 1s ease-out infinite} +@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(34,197,94,.55)}100%{box-shadow:0 0 0 9px rgba(34,197,94,0)}} +@keyframes pulse-red{0%{box-shadow:0 0 0 0 rgba(239,68,68,.6)}100%{box-shadow:0 0 0 9px rgba(239,68,68,0)}} +#liveStatus{flex:none;font-weight:600;color:var(--text);white-space:nowrap} +#chips{flex:1;display:flex;gap:6px;overflow-x:auto;scrollbar-width:thin;padding:2px 0} +#chips::-webkit-scrollbar{height:6px} +#chips::-webkit-scrollbar-thumb{background:var(--rule);border-radius:999px} +.chip{flex:none;display:inline-flex;align-items:center;gap:6px;max-width:260px;border:1px solid var(--rule);background:var(--panel);color:var(--text);border-radius:999px;padding:6px 10px;font:12px/1 var(--font);cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.chip b{color:var(--muted);font-weight:600} +.chip small{color:var(--muted);font-size:11px} +.chip:hover{border-color:var(--accent)} +.chip.cur{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent) inset} +.chip.fresh{animation:land .9s ease-out} +@keyframes land{0%{background:color-mix(in srgb,var(--accent) 45%,transparent)}100%{background:var(--panel)}} +#live .tools{flex:none;display:flex;gap:6px} +#live .tools button{border:1px solid var(--rule);background:transparent;color:var(--muted);border-radius:999px;padding:6px 10px;font:600 12px/1 var(--font);cursor:pointer} +#live .tools button:hover{color:var(--text);border-color:var(--text)} +#live .tools button.on{color:var(--accent);border-color:var(--accent)} +#live .tools button.rec{color:#ef4444;border-color:#ef4444} +#live .tools button:disabled{opacity:.45;cursor:default} +#live .tools button[hidden]{display:none} +#live .tools .ico{display:none;font-size:13px} +#empty{max-width:600px;margin:64px auto;padding:0 18px;text-align:center;color:var(--muted);font:14px/1.6 var(--font)} +#empty b{color:var(--text)} +#empty code{font:13px/1.4 var(--font);color:var(--text);background:var(--panel);border:1px solid var(--rule);border-radius:5px;padding:1px 6px} +#empty a{color:var(--accent)} +#empty .go{display:inline-block;margin-top:14px;padding:9px 16px;border-radius:999px;border:1px solid var(--accent);color:var(--accent);font:700 13px/1 var(--font);text-decoration:none} +[data-live] #controls.off{display:none} +@media (max-width:820px){#live{padding:6px 10px}#liveStatus{display:none}#live .tools button{padding:6px 8px}} +@media (max-width:560px){#bar{gap:6px}#bar .right{gap:2px}#share{padding:8px 10px}.end{display:none}#scrub{width:min(34vw,520px)} + #live .tools .txt{display:none}#live .tools .ico{display:inline}#live .tools button{padding:6px 9px}.chip{max-width:180px}} + +html[data-player] #live{display:none!important} +html[data-player] #controls{display:none!important} +.vp-head{display:flex;align-items:center;justify-content:center;gap:12px;min-width:0} +.vp-kicker{display:flex;align-items:center;gap:8px;min-width:0;margin:0;font:700 12px/1 var(--font);letter-spacing:.12em;text-transform:uppercase;color:var(--muted);white-space:nowrap} +.vp-kicker b{min-width:0;overflow:hidden;text-overflow:ellipsis;color:var(--text);font-size:14px;letter-spacing:.02em;text-transform:none} +.vp-kicker i{display:none} +.vp-step-n{flex:none;padding:7px 13px;border-radius:999px;background:var(--accent);color:var(--bg);font:800 12px/1 var(--font);letter-spacing:.08em;text-transform:uppercase;white-space:nowrap} +.vp-step-n[data-state]{display:inline-flex;align-items:center;gap:7px} +.vp-step-n[data-state]::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor} +.vp-step-n[data-state="watching"]{background:#16a34a;color:#fff} +.vp-step-n[data-state="watching"]::before{box-shadow:0 0 0 3px rgba(255,255,255,.3);animation:vp-live 1.6s ease-in-out infinite} +.vp-step-n[data-state="paused"]{background:transparent;color:var(--muted);box-shadow:inset 0 0 0 1.5px var(--rule)} +.vp-step-n[data-state="replaying"]{background:var(--accent);color:var(--bg)} +.vp-step-n[data-state="recording"]{background:#ef4444;color:#fff} +.vp-step-n[data-state="recording"]::before{border-radius:2px;animation:vp-rec 1s ease-in-out infinite} +.vp-step-n[data-state="disconnected"]{background:var(--muted);color:var(--bg)} +@keyframes vp-live{50%{opacity:.5}} +@media (prefers-reduced-motion:reduce){.vp-step-n[data-state]::before{animation:none!important}} +.vp-step{max-width:1240px;margin:0 auto;padding:20px 18px 0} +.vp-step[hidden]{display:none} +.vp-step-top{display:flex;justify-content:flex-end;margin:0 0 12px} +.vp-labels{padding:6px 12px;border:1px solid var(--rule);border-radius:999px;background:transparent;color:var(--muted);font:600 12px/1 var(--font);cursor:pointer} +.vp-labels:hover{color:var(--text);border-color:var(--text)} +.vp-step-bar{display:flex;gap:6px;margin-bottom:26px} +.vp-step-row{display:flex;align-items:flex-start;gap:10px;margin-bottom:26px} +.vp-step-row .vp-step-bar{flex:1;min-width:0;margin-bottom:0} +.vp-step.compact .vp-step-row{align-items:center} +.vp-watch{flex:none;display:flex;align-items:center;gap:10px;min-width:192px;min-height:52px;margin:2px 0 6px;padding:8px 14px 8px 12px;border:1.5px solid var(--rule);border-radius:10px;background:var(--panel);color:var(--text);text-align:left;font-family:var(--font);cursor:pointer;transition:border-color .15s,background .15s,transform .15s,box-shadow .15s} +.vp-watch[hidden]{display:none} +.vp-watch:hover:not(:disabled){border-color:#22c55e;transform:translateY(-1px);box-shadow:0 6px 14px -8px rgba(0,0,0,.35)} +.vp-watch:focus-visible{outline:2px solid #22c55e;outline-offset:2px} +.vp-watch:disabled{opacity:.4;cursor:default} +.vp-watch.on{border-color:#22c55e;background:color-mix(in srgb,#22c55e 9%,var(--panel))} +.vp-switch{position:relative;flex:none;width:32px;height:18px;border-radius:999px;background:var(--rule);transition:background .2s} +.vp-switch i{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.3);transition:transform .2s} +.vp-watch.on .vp-switch{background:#22c55e} +.vp-watch.on .vp-switch i{transform:translateX(14px)} +.vp-watch .vp-re{display:none;flex:none;color:var(--accent)} +.vp-watch.down .vp-switch{display:none} +.vp-watch.down .vp-re{display:block} +.vp-watch.down:hover:not(:disabled){border-color:var(--accent)} +.vp-watch-text{display:flex;flex-direction:column;gap:4px} +.vp-watch-text b{font:600 12.5px/1.2 var(--font);white-space:nowrap} +.vp-watch-text small{font:600 11px/1 var(--font);letter-spacing:.02em;color:var(--muted);white-space:nowrap} +.vp-step.compact .vp-watch{min-width:158px;min-height:0;margin:0;padding:5px 12px 5px 8px;border-radius:999px} +.vp-step.compact .vp-watch-text small{display:none} +@media (max-width:560px){.vp-watch-text small,.vp-watch .vp-long{display:none}.vp-watch,.vp-step.compact .vp-watch{min-width:0;padding:8px 10px}} +.vp-step-bar button{flex:1;height:8px;padding:0;border:0;border-radius:999px;background:var(--rule);cursor:pointer;transition:background .2s,transform .2s} +.vp-step-bar button.done{background:color-mix(in srgb,var(--accent) 55%,var(--rule))} +.vp-step-bar button.cur{background:var(--accent);transform:scaleY(1.4)} +.vp-step-bar button:hover{background:var(--accent)} +.vp-step-bar:not(.labeled) button>span{display:none} +.vp-step-bar.labeled{gap:8px;overflow-x:auto;overflow-y:hidden;scrollbar-width:thin;padding:2px 2px 6px} +.vp-step-bar.labeled button{flex:1 0 auto;display:flex;flex-direction:column;align-items:flex-start;gap:5px;min-width:112px;max-width:260px;height:auto;padding:7px 11px 9px;border-radius:10px;border-top:4px solid var(--rule);background:var(--panel);text-align:left;transform:none;transition:background .2s} +.vp-step-wait{flex:1;display:flex;align-items:center;height:52px;padding:0 14px;border:1.5px dashed var(--rule);border-radius:10px;color:var(--muted);font:600 12px/1 var(--font)} +.vp-step-bar:not(.labeled) .vp-step-wait{height:8px;padding:0;border:0;border-radius:999px;background:var(--rule);font-size:0} +.vp-step-bar.labeled button.done{background:var(--panel);border-top-color:color-mix(in srgb,var(--accent) 55%,var(--rule))} +.vp-step-bar.labeled button.cur{background:color-mix(in srgb,var(--accent) 12%,var(--panel));border-top-color:var(--accent);box-shadow:0 0 0 1.5px var(--accent) inset;transform:none} +.vp-step-bar.labeled button:hover{background:color-mix(in srgb,var(--accent) 12%,var(--panel))} +.vp-step-bar .seg-meta{font:600 11px/1 var(--font);letter-spacing:.02em;color:var(--muted);white-space:nowrap} +.vp-step-bar .seg-cmd{max-width:100%;font:600 12.5px/1.25 var(--font);color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.vp-controls{display:flex;justify-content:center;padding:4px 18px 28px} +.vp-player{display:flex;flex-direction:column;align-items:center;gap:12px} +.vp-player-main{display:flex;align-items:center;gap:14px} +.vp-player button{font-family:var(--font);cursor:pointer} +.vp-player .vp-skip{display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;border-radius:50%;border:1.5px solid var(--rule);background:var(--panel);color:var(--text);font-size:17px;line-height:1;transition:border-color .15s,color .15s} +.vp-player .vp-skip:hover:not(:disabled){border-color:var(--accent);color:var(--accent)} +.vp-player .vp-skip:disabled{opacity:.35;cursor:default} +.vp-player .vp-play{display:inline-flex;align-items:center;gap:10px;height:56px;padding:0 26px 0 22px;border-radius:999px;border:0;background:var(--accent);color:var(--bg);font-weight:700;font-size:16px;box-shadow:0 12px 24px -12px var(--accent);transition:transform .15s,filter .15s} +.vp-player .vp-play:hover{transform:translateY(-2px);filter:brightness(1.06)} +.vp-player .vp-play span{display:inline-flex} +.vp-player .vp-rec{display:inline-flex;align-items:center;gap:10px;height:56px;padding:0 24px 0 20px;border-radius:999px;border:2px solid #ef4444;background:transparent;color:#ef4444;font-size:16px;font-weight:700;transition:transform .15s,background .15s,color .15s} +.vp-player .vp-rec i{width:16px;height:16px;border-radius:50%;background:#ef4444;transition:border-radius .2s} +.vp-player .vp-rec:hover:not(:disabled){transform:translateY(-2px);background:rgba(239,68,68,.12)} +.vp-player .vp-rec.on{background:#ef4444;color:#fff} +.vp-player .vp-rec.on i{background:#fff;border-radius:3px;animation:vp-rec 1s ease-in-out infinite} +.vp-player .vp-rec:disabled{opacity:.35;cursor:default} +@keyframes vp-rec{50%{opacity:.45}} +@media (prefers-reduced-motion:reduce){.vp-player .vp-rec.on i{animation:none}} +.vp-player-main{flex-wrap:wrap;justify-content:center} +.vp-player .vp-tool{display:inline-flex;align-items:center;gap:8px;height:44px;padding:0 16px 0 14px;border-radius:999px;border:1.5px solid var(--rule);background:var(--panel);color:var(--text);font-size:14px;font-weight:600;white-space:nowrap;transition:border-color .15s,color .15s,background .15s} +.vp-player .vp-tool svg{flex:none} +.vp-player .vp-tool:hover:not(:disabled){border-color:var(--accent);color:var(--accent)} +.vp-player .vp-tool.on{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 12%,var(--panel));color:var(--accent)} +.vp-player .vp-tool:disabled{opacity:.35;cursor:default} +.vp-player .vp-tool[hidden]{display:none} +.vp-player .vp-sep{width:1.5px;height:28px;background:var(--rule)} +.vp-player-main:not(:has(.vp-tool:not([hidden]))) .vp-sep{display:none} +@media (max-width:820px){.vp-kicker{display:none}} +@media (max-width:1100px){.vp-player .vp-tool b{display:none}.vp-player .vp-tool{width:44px;padding:0;justify-content:center}} +@media (max-width:560px){.vp-player-main{gap:8px}.vp-player .vp-sep{display:none}.vp-player .vp-tool{width:38px;height:38px}.vp-player .vp-play,.vp-player .vp-rec{height:46px;padding:0 16px;font-size:15px}.vp-player .vp-rec b{display:none}.vp-player .vp-skip{width:38px;height:38px}} +@media (max-width:440px){.vp-player-main{gap:5px}.vp-player .vp-skip,.vp-player .vp-tool{width:34px;height:34px}.vp-player .vp-play{padding:0 14px 0 12px;gap:6px}.vp-player .vp-rec{padding:0 14px}} +html[data-compact] #bar,html[data-compact] .vp-step{display:none!important} +html[data-compact] body{position:static;padding-bottom:38px} +html[data-compact] #stage{padding:4px 6px 0} +html[data-compact] .vp-controls{position:fixed;left:8px;bottom:8px;padding:0} +html[data-compact] .vp-player .vp-skip,html[data-compact] #vpPlayText{display:none} +html[data-compact] .vp-player .vp-play{justify-content:center;width:30px;height:30px;padding:0;gap:0;font-size:13px;box-shadow:none} +html[data-compact] .vp-player .vp-play:hover{transform:none} +html[data-compact] .vp-player .vp-play span{display:flex} +html[data-compact] .vp-player .vp-play svg{display:block;width:12px;height:12px} +html[data-compact] .vp-player .vp-play path[d^="M7 4v16"]{transform:translateX(-1.5px)} +html[data-compact] #tip{max-width:210px;padding:6px 9px;border-radius:7px;font-size:12px;line-height:1.4} +html[data-compact] #tip .say{margin-top:2px;white-space:normal} +html[data-compact] #tip .say:first-child{margin-top:0} diff --git a/static/git-sim/assets/git-sim-session.js b/static/git-sim/assets/git-sim-session.js new file mode 100644 index 0000000000..9a20e23bf2 --- /dev/null +++ b/static/git-sim/assets/git-sim-session.js @@ -0,0 +1,993 @@ +// Generated by git-sim 0.4.x (https://github.com/initialcommit-com/git-sim) +// from src/git_sim/render/html.py and live_html.py. Do not edit: regenerate it with git-sim. +function makeViewer(root){ + const $ = (s, scope) => Array.from((scope || document).querySelectorAll(s)); + const byId = id => root.querySelector('#' + id); + const embeds = (window.__gitSimEmbeds = window.__gitSimEmbeds || []); + if (root !== document && !embeds.includes(root)) embeds.push(root); + const shown = el => { + const r = el.getBoundingClientRect(); + return Math.max(0, Math.min(r.right, innerWidth) - Math.max(r.left, 0)) * Math.max(0, Math.min(r.bottom, innerHeight) - Math.max(r.top, 0)); + }; + const mine = () => { + if (root === document) return true; + const live = embeds.filter(r => r.isConnected); + const engaged = live.find(r => r.matches(':hover') || r.contains(document.activeElement)); + if (engaged) return engaged === root; + let best = null, most = 0; + live.forEach(r => { const a = shown(r); if (a > most) { best = r; most = a; } }); + return best === root; + }; + const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); + function hashParams(){ + const raw = (location.hash || '').replace(/^#/, ''); + if (!raw.includes('=') || /^step=\d+$/.test(raw)) return {s: raw}; + const out = {}; new URLSearchParams(raw).forEach((v, k) => { out[k] = v; }); return out; + } + function meta(){ + try { return JSON.parse(byId('git-sim-meta').textContent); } catch (e) { return {}; } + } + + const PALETTES = {"dark": {"bg": "#2A2A2A", "text": "#B3B1B1", "text_muted": "#9A9994", "rule": "#5E5E5A", "arrow": "#B3B1B1", "accent": "#D7834F", "commit": "#B3B1B1", "commit_ring": "#B3B1B1", "merge": "#B3B1B1", "merge_ring": "#B3B1B1", "head": "#B3B1B1", "branch": "#B3B1B1", "remote": "#B3B1B1", "tag": "#B3B1B1", "purple": "#007A7E", "gold": "#D7834F", "ref_text": "#B3B1B1", "lane_colors": ["#B3B1B1"], "lane_rings": ["#868585"]}, "light": {"bg": "#F0EFE7", "text": "#000000", "text_muted": "#4E443C", "rule": "#D8D7CF", "arrow": "#000000", "accent": "#F14E32", "commit": "#000000", "commit_ring": "#000000", "merge": "#000000", "merge_ring": "#000000", "head": "#000000", "branch": "#000000", "remote": "#000000", "tag": "#000000", "purple": "#009099", "gold": "#F14E32", "ref_text": "#000000", "lane_colors": ["#000000"], "lane_rings": ["#000000"]}}; + const SVG_NS = 'http://www.w3.org/2000/svg'; + function colorMap(from, to){ + const a = PALETTES[from], b = PALETTES[to], map = {}; + Object.keys(a).forEach(k => { + if (k === 'ref_text') return; + if (Array.isArray(a[k])) a[k].forEach((c, i) => { if (b[k][i]) map[c.toUpperCase()] = b[k][i]; }); + else if (/^#/.test(a[k])) map[a[k].toUpperCase()] = b[k]; + }); + if (!map['#FFFFFF']) map['#FFFFFF'] = to === 'light' ? '#000000' : '#FFFFFF'; + if (!map['#000000']) map['#000000'] = to === 'dark' ? '#FFFFFF' : '#000000'; + return map; + } + function reshade(svg, to){ + const light = to === 'light'; + let defs = svg.querySelector('defs'); + if (!defs) { defs = document.createElementNS(SVG_NS, 'defs'); svg.insertBefore(defs, svg.firstChild); } + const sample = svg.querySelector('feDropShadow'); + let scale = 135; + if (sample) { const sd = parseFloat(sample.getAttribute('stdDeviation')); const wasLight = Math.abs(parseFloat(sample.getAttribute('dy') || '0')) > 0.01; scale = sd / (wasLight ? 0.05 : 0.11) || 135; } + $('filter', defs).forEach(f => f.remove()); + const make = (id, dy, sigma, color, opacity) => { + const f = document.createElementNS(SVG_NS, 'filter'); + f.setAttribute('id', id); f.setAttribute('x', '-60%'); f.setAttribute('y', '-60%'); f.setAttribute('width', '220%'); f.setAttribute('height', '220%'); f.setAttribute('color-interpolation-filters', 'sRGB'); + const d = document.createElementNS(SVG_NS, 'feDropShadow'); + d.setAttribute('dx', '0'); d.setAttribute('dy', String(dy)); d.setAttribute('stdDeviation', String(sigma)); d.setAttribute('flood-color', color); d.setAttribute('flood-opacity', String(opacity)); + f.appendChild(d); defs.appendChild(f); + }; + const own = svg.dataset.uid ? svg.dataset.uid + '-' : ''; + const ensure = (name, ...spec) => { const id = own + name; if (!defs.querySelector('#' + id)) make(id, ...spec); return `url(#${id})`; }; + $('[filter]', svg).forEach(el => { + if (el.dataset.role === 'ref') { if (light) el.setAttribute('filter', ensure('shadow-pill', 0.02 * scale, 0.03 * scale, '#000000', .14)); else el.removeAttribute('filter'); return; } + if (light) el.setAttribute('filter', ensure('shadow-drop', 0.035 * scale, 0.05 * scale, '#000000', .16)); + else { const c = (el.getAttribute('fill') || PALETTES.dark.commit).toUpperCase(); el.setAttribute('filter', ensure('glow-' + c.slice(1), 0, 0.11 * scale, c, .45)); } + }); + if (light) $('rect[data-role="ref"]:not([filter])', svg).forEach(el => el.setAttribute('filter', ensure('shadow-pill', 0.02 * scale, 0.03 * scale, '#000000', .14))); + } + function retheme(svg, from, to){ + if (!svg || from === to || !PALETTES[from] || !PALETTES[to]) return; + const map = colorMap(from, to); + const plain = String(PALETTES[from].ref_text).toUpperCase(); + const plainRefs = Array.from($('text[data-role="ref"]', svg)).filter(t => (t.getAttribute('fill') || '').toUpperCase() === plain); + const attrs = ['fill', 'stroke', 'flood-color', 'data-before-fill', 'data-before-stroke', 'data-after-fill', 'data-after-stroke']; + $('*', svg).forEach(el => attrs.forEach(a => { const v = el.getAttribute(a); if (v) { const m = map[v.toUpperCase()]; if (m) el.setAttribute(a, m); } })); + plainRefs.forEach(t => t.setAttribute('fill', PALETTES[to].ref_text)); + $('[data-role="background"]', svg).forEach(r => r.setAttribute('fill', PALETTES[to].bg)); + $('line[data-role="title"]', svg).forEach(l => l.setAttribute('stroke', PALETTES[to].accent)); + reshade(svg, to); + svg.dataset.theme = to; + } + let themeChoice = ''; + function setTheme(to){ + const svg = byId('scene'); + const info = meta(); + const from = (svg && svg.dataset.theme) || info.svg_theme || info.theme || 'dark'; + retheme(svg, from, to); + document.documentElement.dataset.theme = to; + themeChoice = to; + } + + function siteTheme(){ + const m = /(?:^|;\s*)dark-mode=(true|false)\b/.exec(document.cookie || ''); + return m ? (m[1] === 'true' ? 'dark' : 'light') : ''; + } + + let dispose = null; + let pendingState = null; + let pendingLocked = false; + let control = null; + function mount(svgText, options){ + options = options || {}; + if (dispose) { try { dispose(); } catch (e) {} dispose = null; } + const doc = new DOMParser().parseFromString(svgText, 'image/svg+xml'); + const svgEl = doc.documentElement; + if (svgEl.nodeName !== 'svg') throw new Error('not an svg'); + $('script, foreignObject, animate, animateMotion, animateTransform, set, style, iframe, object, embed', svgEl).forEach(el => el.remove()); + svgEl.querySelectorAll('*').forEach(el => { Array.from(el.attributes).forEach(a => { if (/^on/i.test(a.name) || (/(^|:)href$/i.test(a.name) && !a.value.startsWith('data:image/')) || /javascript:/i.test(a.value)) el.removeAttribute(a.name); }); }); + const uid = 'g' + (window.__gitSimGraphs = (window.__gitSimGraphs || 0) + 1); + const renamed = new Set(); + svgEl.querySelectorAll('[id]').forEach(el => { renamed.add(el.id); el.id = uid + '-' + el.id; }); + if (renamed.size) svgEl.querySelectorAll('*').forEach(el => Array.from(el.attributes).forEach(a => { + if (a.value.indexOf('url(#') < 0) return; + el.setAttribute(a.name, a.value.replace(/url\(#([^)]+)\)/g, (m, id) => renamed.has(id) ? `url(#${uid}-${id})` : m)); + })); + svgEl.dataset.uid = uid; + svgEl.id = 'scene'; + const stage = byId('stage'); + stage.innerHTML = ''; stage.appendChild(document.importNode(svgEl, true)); + if (options.title) { + const el = byId('git-sim-meta'); + if (el) { try { const j = JSON.parse(el.textContent); j.title = options.title; el.textContent = JSON.stringify(j); } catch (e) {} } + } + const info = meta(); + const shown = byId('scene'); + shown.dataset.theme = shown.dataset.theme || options.svgTheme || info.svg_theme || info.theme || 'dark'; + const hashTheme = /^(dark|light)$/.test(hashParams().m || '') ? hashParams().m : ''; + const want = siteTheme() || hashTheme || themeChoice || info.theme; + if (want && shown.dataset.theme !== want) retheme(shown, shown.dataset.theme, want); + if (want) document.documentElement.dataset.theme = want; + pendingState = options.state || null; + pendingLocked = !!options.locked; + try { init(); } finally { pendingState = null; pendingLocked = false; } + } + + function init(){ + const svg = byId('scene'); + if (!svg) return; + const stage = byId('stage'); + const tip = byId('tip'); + const info = meta(); + const offs = []; + const on = (target, type, fn, opts) => { target.addEventListener(type, fn, opts); offs.push(() => target.removeEventListener(type, fn, opts)); }; + const pristine = svg.outerHTML; + const pristineTheme = svg.dataset.theme || meta().theme || 'dark'; + + const after = $('[data-phase="after"]', svg); + const removed = $('[data-phase="removed"]', svg); + const moved = $('[data-dx]', svg); + const recolored = $('[data-before-fill], [data-before-dash]', svg); + let maxStep = 1; + [...after, ...removed, ...moved, ...recolored].forEach(el => { const s = parseInt(el.dataset.step || '0', 10); if (s > maxStep) maxStep = s; }); + const stepOf = el => parseInt(el.dataset.step || '0', 10) || maxStep; + const amount = (el, p) => clamp(p - (stepOf(el) - 1), 0, 1); + const hex = c => { const m = /^#?([0-9a-f]{6})$/i.exec((c || '').trim()); return m ? [0, 2, 4].map(i => parseInt(m[1].slice(i, i + 2), 16)) : null; }; + const mix = (a, b, t) => { const A = hex(a), B = hex(b); if (!A || !B) return t < .5 ? a : b; return '#' + A.map((v, i) => Math.round(v + (B[i] - v) * t).toString(16).padStart(2, '0')).join(''); }; + recolored.forEach(el => { el.dataset.afterFill = el.getAttribute('fill') || ''; el.dataset.afterStroke = el.getAttribute('stroke') || ''; el.dataset.afterDash = el.getAttribute('stroke-dasharray') || ''; }); + + const commitOf = new Map(); + $('[data-role="commit"]', svg).forEach(el => { if (el.dataset.sha) commitOf.set(el.dataset.sha, el); }); + const hits = new Map(); + { + const boxes = new Map(); + $('[data-role="commit"],[data-role="commit-label"]', svg).forEach(el => { + const sha = el.dataset.sha; + if (!sha) return; + let b; + try { b = el.getBBox(); } catch (e) { return; } + if (!b || (!b.width && !b.height)) return; + const cur = boxes.get(sha) || {x: Infinity, y: Infinity, r: -Infinity, b: -Infinity}; + cur.x = Math.min(cur.x, b.x); cur.y = Math.min(cur.y, b.y); + cur.r = Math.max(cur.r, b.x + b.width); cur.b = Math.max(cur.b, b.y + b.height); + boxes.set(sha, cur); + }); + const anchor = svg.querySelector('[data-role="background"]') || svg.querySelector('defs'); + boxes.forEach((b, sha) => { + const pad = 6, hit = document.createElementNS(SVG_NS, 'rect'); + hit.setAttribute('x', b.x - pad); hit.setAttribute('y', b.y - pad); + hit.setAttribute('width', b.r - b.x + 2 * pad); hit.setAttribute('height', b.b - b.y + 2 * pad); + hit.setAttribute('fill', 'transparent'); + hit.dataset.role = 'commit-hit'; hit.dataset.sha = sha; + if (anchor) anchor.after(hit); else svg.prepend(hit); + hits.set(sha, hit); + }); + } + const syncHits = () => hits.forEach((hit, sha) => { const c = commitOf.get(sha); hit.style.pointerEvents = c && parseFloat(c.style.opacity || '1') < .5 ? 'none' : ''; }); + + const scrub = byId('scrub'); + const stepLabel = byId('stepLabel'); + const showSteps = !!document.documentElement.dataset.live; + stepLabel.hidden = !showSteps; + if (!showSteps) stepLabel.textContent = ''; + const btnBefore = byId('toBefore'), btnAfter = byId('toAfter'); + const play = byId('play'); + const RES = 1000; + scrub.max = String(maxStep * RES); + let progress = 0; + + const trailFade = el => { + const s = stepOf(el); + return s >= maxStep ? [s - 0.15, 0.15] : [s + 0.35, 0.65]; + }; + const isOrigin = el => el.dataset.role === 'edge' && el.dataset.kind === 'origin'; + const isLane = el => el.dataset.role === 'edge' && el.dataset.kind !== 'origin'; + const heads = new Map(); + const isCurve = el => el.tagName === 'path' || el.tagName === 'polyline'; + after.filter(el => isLane(el) && (el.tagName === 'line' || isCurve(el))).forEach(el => { + let len = 0; + try { len = isCurve(el) ? el.getTotalLength() : Math.hypot(el.x2.baseVal.value - el.x1.baseVal.value, el.y2.baseVal.value - el.y1.baseVal.value); } catch (e) {} + if (len > 0) el.dataset.len = len; + const head = el.nextElementSibling; + if (len > 0 && head && head.tagName === 'polygon' && isLane(head) && head.dataset.step === el.dataset.step) heads.set(head, el); + }); + const pointAt = (line, t) => { + if (isCurve(line)) { const p = line.getPointAtLength(parseFloat(line.dataset.len) * t); return [p.x, p.y]; } + const x1 = line.x1.baseVal.value, y1 = line.y1.baseVal.value; + return [x1 + (line.x2.baseVal.value - x1) * t, y1 + (line.y2.baseVal.value - y1) * t]; + }; + const pushed = new Map(); + { + const files = moved.filter(el => el.dataset.role === 'file' && el.dataset.phase === 'after'); + heads.forEach((line, head) => { + if (line.dataset.src || line.tagName !== 'line') return; + const [ex, ey] = pointAt(line, 1), dir = Math.sign(line.x2.baseVal.value - line.x1.baseVal.value) || 1; + let reach = 40; try { reach = head.getBBox().width * 2.5 + 10; } catch (e) {} + let best = null, bestGap = Infinity; + files.forEach(f => { + let b; try { b = f.getBBox(); } catch (e) { return; } + if (!b.width || Math.abs(b.y + b.height / 2 - ey) > b.height * 0.6) return; + const gap = dir > 0 ? b.x - ex : ex - (b.x + b.width); + if (gap >= 0 && gap < reach && gap < bestGap) { best = f; bestGap = gap; } + }); + if (best) pushed.set(best, line); + }); + } + function placeHead(head, line, t){ + if (t >= 1) { head.removeAttribute('transform'); return; } + const [ex, ey] = pointAt(line, 1), [px, py] = pointAt(line, t); + let turn = ''; + if (isCurve(line)) { + const [qx, qy] = pointAt(line, Math.max(t - 0.02, 0)), [rx, ry] = pointAt(line, 0.98); + const deg = (Math.atan2(py - qy, px - qx) - Math.atan2(ey - ry, ex - rx)) * 180 / Math.PI; + turn = ` rotate(${deg} ${ex} ${ey})`; + } + head.setAttribute('transform', `translate(${px - ex} ${py - ey})${turn}`); + } + const slides = new Set(moved.filter(el => el.dataset.phase === 'after')); + function render(){ + after.forEach(el => { + const a = amount(el, progress); + let o = a; + if (isOrigin(el)) { + const t = el.dataset.t !== undefined ? parseFloat(el.dataset.t) : 1; + o = a > 0 && a >= t - 1e-6 ? 1 : 0; + const [from, span] = trailFade(el); + o *= 1 - clamp((progress - from) / span, 0, 1); + } else if (heads.has(el)) { + placeHead(el, heads.get(el), a); + o = a > 0 ? 1 : 0; + } else if (isLane(el)) { + const len = parseFloat(el.dataset.len || '0'); + if (len > 0) { el.style.strokeDasharray = `${len}`; el.style.strokeDashoffset = `${len * (1 - a)}`; o = a > 0 ? 1 : 0; } + else o = a > .85 ? 1 : 0; + } else if (slides.has(el)) { + o = clamp(a / (pushed.has(el) ? 0.3 : 0.15), 0, 1); + } + el.style.opacity = o; el.style.pointerEvents = o < .5 ? 'none' : ''; + }); + removed.forEach(el => { const a = 1 - amount(el, progress); el.style.opacity = a; el.style.pointerEvents = a < .5 ? 'none' : ''; }); + syncHits(); + moved.forEach(el => { + const a = amount(el, progress), back = 1 - a; + const line = pushed.get(el); + if (line) { + const [ex, ey] = pointAt(line, 1), [px, py] = pointAt(line, a); + el.style.transform = back > 0 ? `translate(${px - ex}px, ${py - ey}px)` : ''; + return; + } + el.style.transform = back > 0 ? `translate(${el.dataset.dx * back}px, ${el.dataset.dy * back}px)` : ''; + }); + recolored.forEach(el => { + const a = amount(el, progress); + const hollow = el.dataset.afterFill === 'none' || el.dataset.beforeFill === 'none' || el.getAttribute('fill') === 'none'; + if (el.dataset.afterFill && el.dataset.beforeFill !== undefined && !hollow) el.setAttribute('fill', mix(el.dataset.beforeFill, el.dataset.afterFill, a)); + if (el.dataset.afterStroke && el.dataset.beforeFill !== undefined) el.setAttribute('stroke', mix(el.dataset.beforeStroke || el.dataset.beforeFill, el.dataset.afterStroke, a)); + if (el.dataset.afterDash || el.dataset.beforeDash !== undefined) el.setAttribute('stroke-dasharray', a < .5 ? (el.dataset.beforeDash || 'none') : (el.dataset.afterDash || 'none')); + }); + scrub.value = String(Math.round(progress * RES)); + scrub.style.setProperty('--pct', (100 * progress / maxStep) + '%'); + btnBefore.classList.toggle('on', progress <= 0.001); + btnAfter.classList.toggle('on', progress >= maxStep - 0.001); + if (showSteps && maxStep > 1) { + stepLabel.textContent = progress <= 0.001 ? 'before' : progress >= maxStep - 0.001 ? 'after' : `step ${Math.ceil(progress - 0.001)} / ${maxStep}`; + } + } + function setProgress(p){ progress = clamp(p, 0, maxStep); render(); } + + let playing = false, raf = null, hold = null; + const ease = t => t < .5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; + const compactRun = !!document.documentElement.dataset.compact; + const stepwise = x => { const k = Math.floor(x); return k + ease(x - k); }; + const unstep = v => { const k = Math.floor(v), f = v - k; + return k + (f < .5 ? Math.sqrt(f / 2) : 1 - Math.sqrt(2 * (1 - f)) / 2); }; + function tween(to, ms, then){ + const from = compactRun ? unstep(progress) : progress, t0 = performance.now(); + const frame = now => { + if (!playing) return; + const t = clamp((now - t0) / ms, 0, 1); + setProgress(compactRun ? stepwise(from + (to - from) * t) : from + (to - from) * ease(t)); + if (t < 1) raf = requestAnimationFrame(frame); else if (then) hold = setTimeout(then, then === backward ? 1400 : 1000); + }; + raf = requestAnimationFrame(frame); + } + const perStep = compactRun ? 1000 : clamp(4500 / maxStep, 380, 900); + function forward(){ tween(maxStep, perStep * Math.max(1, maxStep - progress) + 200, backward); } + function backward(){ tween(0, 700 + 120 * maxStep, forward); } + function startPlay(){ + if (playing) return; + playing = true; play.innerHTML = '❚❚'; play.title = 'pause (A)'; + if (progress >= maxStep - 0.001) { setProgress(0); } + forward(); + } + function stopPlay(){ + playing = false; cancelAnimationFrame(raf); clearTimeout(hold); + play.innerHTML = '▶'; play.title = 'play the command from before to after, on a loop (A)'; + } + let onceThen = null, oncePaused = false; + function playOnce(then){ + stopPlay(); + if (!animatable) { if (then) then(); return; } + setProgress(0); + onceThen = then || null; oncePaused = false; + playing = true; play.innerHTML = '❚❚'; play.title = 'pause (A)'; + tween(maxStep, perStep * maxStep + 200, finishOnce); + } + function finishOnce(){ stopPlay(); const then = onceThen; onceThen = null; if (then) then(); } + function pauseOnce(){ + if (!playing || !onceThen) return false; + playing = false; cancelAnimationFrame(raf); clearTimeout(hold); oncePaused = true; + play.innerHTML = '▶'; + return true; + } + function resumeOnce(){ + if (!oncePaused) return false; + oncePaused = false; playing = true; play.innerHTML = '❚❚'; + tween(maxStep, perStep * Math.max(0.3, maxStep - progress) + 200, finishOnce); + return true; + } + let locked = false; + const controls = byId('controls'); + function setLocked(v){ + locked = !!v; + if (locked) stopPlay(); + controls.classList.toggle('locked', locked); + controls.title = locked ? 'Type the command in the terminal to play it' : ''; + [play, btnBefore, btnAfter, scrub].forEach(el => { el.disabled = locked; }); + } + control = { + playOnce, pauseOnce, resumeOnce, setLocked, + setState: s => { stopPlay(); setProgress(s === 'after' ? maxStep : s === 'before' ? 0 : clamp(parseInt(String(s).replace(/^step=/, ''), 10) || 0, 0, maxStep)); }, + setProgress: p => { stopPlay(); setProgress(p); }, + steps: () => maxStep, + animatable: () => animatable, + }; + const manual = fn => (...args) => { if (locked) return; stopPlay(); fn(...args); }; + play.onclick = () => { if (locked) return; playing ? stopPlay() : startPlay(); }; + btnBefore.onclick = manual(() => setProgress(0)); + btnAfter.onclick = manual(() => setProgress(maxStep)); + on(scrub, 'input', manual(() => setProgress(parseInt(scrub.value, 10) / RES))); + on(scrub, 'pointerdown', () => stopPlay()); + + const animatable = after.length + removed.length + moved.length + recolored.length > 0; + controls.classList.toggle('off', !animatable); + if (pendingLocked) setLocked(true); + const params = hashParams(); + const state = pendingState !== null ? pendingState : (params.s || ''); + const pinned = /^(before|after|step=\d+)$/.test(state); + if (state === 'after' || !animatable) progress = maxStep; + else if (/^step=\d+$/.test(state)) progress = clamp(parseInt(state.slice(5), 10), 0, maxStep); + else progress = 0; + render(); + if (animatable && !pinned) startPlay(); + + on(document, 'keydown', e => { + if (!mine()) return; + const t = e.target; + if (t && t !== scrub && (/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName) || t.isContentEditable)) return; + if (e.target === scrub && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) { e.preventDefault(); } + if (e.key === 'Escape') { stopPlay(); resetView(); return; } + if (!animatable || locked) return; + if (e.key === 'a' || e.key === 'A') { playing ? stopPlay() : startPlay(); return; } + if (e.key === 'ArrowLeft') manual(() => setProgress(Math.ceil(progress - 0.001) - 1))(); + else if (e.key === 'ArrowRight') manual(() => setProgress(Math.floor(progress + 0.001) + 1))(); + else if (e.key === ' ' && (root !== document || document.documentElement.dataset.mode !== 'demo')) { e.preventDefault(); manual(() => setProgress(progress >= maxStep - 0.001 ? 0 : maxStep))(); } + }); + + const drawn = new Set($('[data-role="commit"]', svg).map(el => el.dataset.sha)); + const parents = {}; + const link = (a, b) => { if (!a || !b || a === b || !drawn.has(b)) return; const list = parents[a] = parents[a] || []; if (!list.includes(b)) list.push(b); }; + $('[data-role="edge"]', svg).forEach(el => link(el.dataset.src, el.dataset.dst)); + $('[data-role="commit"]', svg).forEach(el => (el.dataset.parents || '').split(/\s+/).filter(Boolean).forEach(q => link(el.dataset.sha, q))); + function ancestry(sha){ + const seen = new Set(); const stack = [sha]; + while (stack.length) { const s = stack.pop(); if (seen.has(s)) continue; seen.add(s); (parents[s] || []).forEach(p => stack.push(p)); } + return seen; + } + function highlight(sha){ + const keep = ancestry(sha); + $('[data-role="commit"],[data-role="commit-label"]', svg).forEach(el => el.classList.toggle('dim', !keep.has(el.dataset.sha))); + $('[data-role="edge"]', svg).forEach(el => { + const linked = keep.has(el.dataset.src) && keep.has(el.dataset.dst); + el.classList.toggle('dim', !linked); + el.classList.toggle('lit', isOrigin(el) && amount(el, progress) > .5 && (el.dataset.src === sha || el.dataset.dst === sha)); + }); + } + function clearHighlight(){ $('.dim,.lit', svg).forEach(el => el.classList.remove('dim', 'lit')); } + const esc = s => String(s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); + const compactPage = !!document.documentElement.dataset.compact; + function labelOf(sha){ + const label = $('[data-role="commit-label"][data-sha="' + sha + '"]', svg)[0]; + const commit = $('[data-role="commit"][data-sha="' + sha + '"]', svg)[0]; + return (label && label.textContent.trim()) || (commit && commit.dataset.message) || String(sha).slice(0, 7); + } + function describe(el){ + const d = el.dataset; + if (d.role === 'commit' || d.role === 'commit-label' || d.role === 'commit-hit') { + const c = $('[data-role="commit"][data-sha="' + d.sha + '"]', svg)[0]; + const data = c ? c.dataset : d; + if (compactPage && data.kind !== 'elided') { + const label = labelOf(data.sha), parents = (data.parents || '').split(/\s+/).filter(Boolean).map(labelOf); + const copied = /[′']$/.test(label) ? label.replace(/[′']+$/, '') : ''; + const said = data.kind === 'merge' ? 'A merge commit: it has two parents, one from each branch.' + : copied ? `A copy of ${copied}: the same changes, on a new parent.` + : data.lost ? 'This commit is no longer reachable from any branch and will eventually be garbage collected.' + : ''; + return `
${esc(label)}
` + + `
${parents.length > 1 ? 'parents' : 'parent'} ${esc(parents.join(', ') || 'none')}
` + + (said ? `
${esc(said)}
` : ''); + } + if (data.kind === 'elided') return `
${esc(data.message || '')}
stands for the commits between its neighbours
`; + return `
${esc(data.sha)}
${esc(data.message || '')}
` + + `
author ${esc(data.author || '')}
` + + (data.date ? `
date ${esc(data.date)}
` : '') + + `
parents ${esc((data.parents || '').split(/\s+/).filter(Boolean).map(p => p.slice(0, 7)).join(', ') || 'none')}
` + + (data.kind === 'merge' ? '
merge commit
' : '') + + (data.lost ? '
unreachable commit
' : '') + + (data.phase === 'after' ? '
simulated by this command
' : '') + + '
click to copy sha
'; + } + if (compactPage && d.role === 'ref') { + const name = '' + esc(d.name) + ''; + if (d.name === 'HEAD') return '
Git HEAD always points to the currently checked-out branch/commit.
'; + if (d.kind === 'tag') return '
The ' + name + ' tag. In Git, a tag is a label that stays on one commit.
'; + if (d.kind === 'remote') return '
The ' + name + ' remote-tracking branch: where that branch was on the remote when you last fetched.
'; + return '
The ' + name + ' branch. In Git, a branch is just a label that points to a commit.
'; + } + if (compactPage && d.role === 'file') { + const col = String(d.column || '').toLowerCase(), name = '' + esc(d.name) + ''; + const said = /stag/.test(col) ? name + ' is staged: its changes go into the next commit.' + : /modif/.test(col) ? name + " has changes that aren't staged yet." + : /untrack/.test(col) ? "Git isn't tracking " + name + ' yet.' + : name; + return '
' + said + '
'; + } + if (d.role === 'ref') return`
${esc(d.name)}
${esc(d.kind || 'ref')}${d.phase === 'after' ? ' · simulated' : d.phase === 'removed' ? ' · removed by this command' : ''}
`; + if (d.role === 'file') return `
${esc(d.name)}
${esc(d.column || '')}${d.phase === 'after' ? ' · after the command' : ''}
`; + return ''; + } + let hoverSha = null, clearTimer = null; + function clearNow(){ clearTimeout(clearTimer); clearTimer = null; hoverSha = null; tip.style.display = 'none'; clearHighlight(); } + function scheduleClear(){ if (clearTimer === null) clearTimer = setTimeout(clearNow, 320); } + svg.addEventListener('mousemove', e => { + const el = e.target.closest('[data-role="commit"],[data-role="commit-label"],[data-role="commit-hit"],[data-role="ref"],[data-role="file"]'); + const shown = el && (el.dataset.role === 'commit-hit' ? commitOf.get(el.dataset.sha) || el : el); + if (!el || parseFloat(shown.style.opacity || '1') < .5) { tip.style.display = 'none'; scheduleClear(); return; } + const content = describe(el); + if (!content) { tip.style.display = 'none'; scheduleClear(); return; } + clearTimeout(clearTimer); clearTimer = null; + tip.innerHTML = content; tip.style.display = 'block'; + const x = Math.min(e.clientX + 16, window.innerWidth - tip.offsetWidth - 12); + const y = Math.min(e.clientY + 16, window.innerHeight - tip.offsetHeight - 12); + tip.style.left = x + 'px'; tip.style.top = y + 'px'; + const sha = el.dataset.sha || null; + if (sha !== hoverSha) { hoverSha = sha; if (sha) highlight(sha); else clearHighlight(); } + }); + svg.addEventListener('mouseleave', clearNow); + svg.addEventListener('click', e => { + const el = e.target.closest('[data-sha]'); + if (!el || !navigator.clipboard || compactPage) return; + navigator.clipboard.writeText(el.dataset.sha).then(() => { + tip.innerHTML = `
${esc(el.dataset.sha)}
copied
`; + clearTimeout(tipTimer); tipTimer = setTimeout(() => { tip.style.display = 'none'; }, 900); + }).catch(() => {}); + }); + + const base = svg.viewBox.baseVal; + const vb0 = {x: base.x, y: base.y, w: base.width, h: base.height}; + let vb = {...vb0}; + function setView(){ svg.setAttribute('viewBox', `${vb.x} ${vb.y} ${vb.w} ${vb.h}`); } + function resetView(){ vb = {...vb0}; setView(); } + function toScene(cx, cy){ + const r = svg.getBoundingClientRect(); + const k = Math.max(vb.w / r.width, vb.h / r.height); + const ox = (r.width - vb.w / k) / 2, oy = (r.height - vb.h / k) / 2; + return {x: vb.x + (cx - r.left - ox) * k, y: vb.y + (cy - r.top - oy) * k, k}; + } + on(stage, 'wheel', e => { + if (!(e.ctrlKey || e.metaKey)) return; + e.preventDefault(); + const f = Math.exp(e.deltaY * 0.0015); + const p = toScene(e.clientX, e.clientY); + const nw = Math.min(vb0.w * 4, Math.max(vb0.w / 12, vb.w * f)); + const s = nw / vb.w; + vb = {x: p.x - (p.x - vb.x) * s, y: p.y - (p.y - vb.y) * s, w: nw, h: vb.h * s}; + setView(); + }, {passive: false}); + on(stage, 'dblclick', resetView); + + function fit(){ + if (document.documentElement.dataset.mode === 'demo') { svg.style.height = ''; return; } + const natural = stage.clientWidth * vb0.h / vb0.w; + const top = stage.getBoundingClientRect().top + window.scrollY; + const available = window.innerHeight - top - 24; + svg.style.height = (available > 240 && natural > available) ? Math.max(available, natural * 0.55) + 'px' : ''; + } + fit(); + on(window, 'resize', fit); + on(window, 'git-sim:layout', fit); + + const help = byId('help'), helpMenu = byId('helpMenu'); + const showHelp = on => { helpMenu.hidden = !on; help.classList.toggle('on', on); }; + help.onclick = e => { e.stopPropagation(); showHelp(helpMenu.hidden); }; + on(document, 'click', e => { if (!helpMenu.hidden && !helpMenu.contains(e.target)) showHelp(false); }); + on(document, 'keydown', e => { if (!mine()) return; if (e.key === 'Escape') showHelp(false); if (e.key === '?') showHelp(helpMenu.hidden); }); + + const shareBtn = byId('share'), shareMenu = byId('shareMenu'); + const toast = byId('toast'); + let toastTimer = null; + const say = msg => { toast.textContent = msg; toast.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove('show'), 1900); }; + const showShare = on => { shareMenu.hidden = !on; shareBtn.classList.toggle('on', on); if (on) showHelp(false); }; + shareBtn.onclick = e => { e.stopPropagation(); showShare(shareMenu.hidden); }; + on(document, 'click', e => { if (!shareMenu.hidden && !shareMenu.contains(e.target)) showShare(false); }); + on(document, 'keydown', e => { if (e.key === 'Escape') showShare(false); }); + const title = (info.title && info.title !== 'git-sim' ? info.title : '') || params.t || document.title; + if (params.t && (!info.title || info.title === 'git-sim')) document.title = params.t + ', created with git-sim'; + + const fileName = () => (title.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '') || 'git-sim'); + async function toPng(scale){ + const clone = svg.cloneNode(true); + clone.setAttribute('viewBox', `${vb0.x} ${vb0.y} ${vb0.w} ${vb0.h}`); + clone.setAttribute('width', Math.round(vb0.w * scale)); clone.setAttribute('height', Math.round(vb0.h * scale)); + const url = URL.createObjectURL(new Blob([new XMLSerializer().serializeToString(clone)], {type: 'image/svg+xml;charset=utf-8'})); + try { + const img = await new Promise((ok, fail) => { const i = new Image(); i.onload = () => ok(i); i.onerror = fail; i.src = url; }); + const canvas = document.createElement('canvas'); + canvas.width = Math.round(vb0.w * scale); canvas.height = Math.round(vb0.h * scale); + canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height); + return await new Promise(ok => canvas.toBlob(ok, 'image/png')); + } finally { URL.revokeObjectURL(url); } + } + const editor = typeof acquireVsCodeApi === 'function' ? (window.__gitSimHost = window.__gitSimHost || acquireVsCodeApi()) : null; + const toEditor = (blob, name) => new Promise(ok => { + const reader = new FileReader(); + reader.onload = () => { editor.postMessage({type: 'saveFile', name, mime: blob.type, data: String(reader.result).split(',')[1]}); ok(); }; + reader.readAsDataURL(blob); + }); + const download = (blob, name) => { if (editor) return toEditor(blob, name); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 3000); }; + const actions = { + image: async () => { + const png = await toPng(2); + if (editor) { await download(png, fileName() + '.png'); say('the editor cannot copy an image: save the PNG instead'); return; } + try { await navigator.clipboard.write([new ClipboardItem({'image/png': png})]); say('image copied, paste it anywhere'); } + catch (e) { download(png, fileName() + '.png'); say('image downloaded'); } + }, + png: async () => { await download(await toPng(2), fileName() + '.png'); if (!editor) say('PNG downloaded'); }, + svg: async () => { await download(new Blob([pristine], {type: 'image/svg+xml'}), fileName() + '.svg'); if (!editor) say('SVG downloaded'); }, + html: async () => { await download(new Blob(['\n' + document.documentElement.outerHTML], {type: 'text/html'}), fileName() + '.html'); if (!editor) say('page downloaded'); }, + }; + on(shareMenu, 'click', e => { + const b = e.target.closest('[data-action]'); + if (!b) return; + e.preventDefault(); + Promise.resolve().then(() => actions[b.dataset.action]()).catch(() => say('could not share')); + }); + + dispose = () => { stopPlay(); clearNow(); showHelp(false); showShare(false); offs.forEach(off => off()); offs.length = 0; control = null; }; + } + + function playOnce(then){ if (control) control.playOnce(then); else if (then) then(); } + function pauseOnce(){ return !!(control && control.pauseOnce()); } + function resumeOnce(){ return !!(control && control.resumeOnce()); } + function setState(s){ if (control) control.setState(s); } + function setLocked(v){ if (control) control.setLocked(v); } + function setProgress(p){ if (control) control.setProgress(p); } + function steps(){ return control ? control.steps() : 0; } + function animatable(){ return !!(control && control.animatable()); } + + return {init, mount, setTheme, retheme, playOnce, pauseOnce, resumeOnce, setState, setLocked, setProgress, steps, animatable, root}; +} +window.GitSimViewer = Object.assign(makeViewer(document), {instance: makeViewer}); + +(function(){ + const V = window.GitSimViewer; + const inVscode = typeof acquireVsCodeApi === 'function'; + const host = inVscode ? (window.__gitSimHost = window.__gitSimHost || acquireVsCodeApi()) : null; + const $ = id => document.getElementById(id); + const el = id => $(id) || document.createElement('span'); + const dot = el('liveDot'), status = el('liveStatus'), chips = $('chips'); + const replayBtn = el('replay'), followBtn = el('follow'), clearBtn = el('clear'), saveBtn = el('save'), recordBtn = el('record'); + const info = (() => { try { return JSON.parse($('git-sim-meta').textContent); } catch (e) { return {}; } })(); + const recorded = (() => { const el = $('git-sim-session'); if (!el) return null; try { return JSON.parse(el.textContent); } catch (e) { return null; } })(); + const stepSvg = i => { const el = $('git-sim-step-' + i.index); return el ? el.textContent : ''; }; + const frag = new URLSearchParams((location.hash || '').replace(/^#/, '')); + const base = (frag.get('live') || '').replace(/\/+$/, ''); + const key = frag.get('k') || info.key || ''; + const hosted = !!base; + const api = path => base + path + (path.includes('?') ? '&' : '?') + 'k=' + encodeURIComponent(key); + const items = []; + const svgs = new Map(); + let current = -1, follow = true, replaying = false, connected = false, recording = false; + let downNote = ''; + const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); + const clock = t => { const d = new Date(t * 1000); return d.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', second: '2-digit'}); }; + const repoName = () => (recorded && recorded.repo) || info.repo || ''; + const whereOf = () => (recorded && recorded.where) || info.where || ''; + let lastError = ''; + const complain = text => { lastError = 'error: ' + String(text).slice(0, 160); status.textContent = lastError; dot.className = 'off'; }; + window.addEventListener('error', e => complain(e.message || e.type)); + window.addEventListener('unhandledrejection', e => complain((e.reason && e.reason.message) || e.reason || 'promise rejected')); + + function setStatus(){ + const repo = repoName() ? ' · ' + repoName() : ''; + const changes = items.filter(i => i.index !== 0).length; + dot.className = recording ? 'rec' : recorded ? '' : connected ? (follow && !replaying ? 'on' : '') : 'off'; + if (lastError) { status.textContent = lastError; dot.className = 'off'; } + else if (recording) status.textContent = recording; + else if (recorded && recorded.title) status.textContent = `${recorded.title} · ${changes} step${changes === 1 ? '' : 's'}`; + else if (recorded) status.textContent = `recorded${repo} · ${changes} change${changes === 1 ? '' : 's'}` + (recorded.started ? ' · ' + new Date(recorded.started * 1000).toLocaleString([], {dateStyle: 'medium', timeStyle: 'short'}) : ''); + else if (!connected) status.textContent = (downNote || 'disconnected') + repo; + else if (replaying) status.textContent = `replaying ${current} / ${changes}`; + else if (follow) status.textContent = 'live' + repo; + else status.textContent = `paused at ${current} of ${changes}` + repo; + status.dataset.state = recorded ? '' : !connected ? 'disconnected' : recording ? 'recording' : replaying ? 'replaying' : follow ? 'watching' : 'paused'; + status.dataset.down = downNote ? '1' : ''; + status.dataset.where = whereOf(); + followBtn.classList.toggle('on', follow && !recorded && connected); + followBtn.title = !recorded && !connected ? 'reconnect to git-sim live and jump to each change as it happens (L)' : 'jump to each change as it happens (L)'; + replayBtn.classList.toggle('on', replaying); + const busy = !!recording; + replayBtn.disabled = busy || items.filter(i => i.index !== 0).length < (recorded ? 1 : 2); + clearBtn.disabled = busy || items.length < 2; + followBtn.disabled = busy; + if (saveBtn) saveBtn.disabled = busy || items.length < 1; + if (recordBtn) { recordBtn.disabled = items.length < 1 || !window.MediaRecorder; recordBtn.classList.toggle('rec', busy); } + document.title = recorded && recorded.title + ? (items[current] ? items[current].label + ' - ' : '') + recorded.title + ' (git-sim)' + : (items[current] ? items[current].label + ' - ' : '') + 'git-sim live' + (repoName() ? ': ' + repoName() : ''); + document.dispatchEvent(new CustomEvent('git-sim:status')); + } + function chipFor(item){ + const b = document.createElement('button'); + b.className = 'chip'; b.dataset.i = item.index; + const when = item.time ? clock(item.time) : ''; + b.title = (item.detail || item.label) + (when ? '\n' + when : '') + (recorded ? '' : '\nshift+click opens this change as a page of its own'); + b.innerHTML = (item.index === 0 ? 'start ' : `${item.index} `) + esc(item.label) + (when ? ` ${when}` : ''); + b.addEventListener('click', e => { + if (recording) return; + if (e.shiftKey && !recorded) { + if (inVscode) host.postMessage({type: 'openPage', page: item.page}); else window.open(api(`/page/${item.index}`), '_blank'); + return; + } + replaying = false; + follow = item.index === items[items.length - 1].index; + show(item.index, true); + }); + return b; + } + async function svgOf(index){ + if (svgs.has(index)) return svgs.get(index); + if (inVscode || recorded) return null; + const r = await fetch(api(`/svg/${index}`)); + if (!r.ok) throw new Error(`the graph for change ${index} could not be fetched (${r.status})`); + const text = await r.text(); + svgs.set(index, text); + return text; + } + const trace = frag.has('debug') ? (m => { lastError = 'trace: ' + m; status.textContent = lastError; }) : () => {}; + let showToken = 0; + async function show(index, play, then, wait){ + const token = ++showToken; + const item = items.find(i => i.index === index); + if (!item) return; + trace(`fetching ${index}`); + const svg = await svgOf(index); + trace(`fetched ${index}: ${svg ? svg.length : 0} chars`); + if (!svg || token !== showToken) return; + current = index; + Array.from(chips.children).forEach(c => c.classList.toggle('cur', Number(c.dataset.i) === index)); + const chip = chips.querySelector(`.chip[data-i="${index}"]`); + if (chip) { const cr = chip.getBoundingClientRect(), sr = chips.getBoundingClientRect(); + if (cr.left < sr.left) chips.scrollLeft += cr.left - sr.left; else if (cr.right > sr.right) chips.scrollLeft += cr.right - sr.right; } + const empty = $('empty'); if (empty) empty.remove(); + const hold = play && wait > 0; + try { + V.mount(svg, {title: item.label, state: hold ? 'before' : 'after'}); + } catch (e) { complain(`could not draw change ${index}: ${e.message}`); return; } + trace(`mounted ${index}: stage has ${$('stage').children.length} child(ren)`); + setStatus(); + if (hold) setTimeout(() => { if (token === showToken) V.playOnce(then); }, wait); + else if (play) V.playOnce(then); else if (then) then(); + } + function add(item, svg, fresh){ + if (items.some(i => i.index === item.index)) return; + items.push(item); + items.sort((a, b) => a.index - b.index); + if (svg && /^\s* i.index).filter(i => i !== 0); + if (!order.length || (items.length < 2 && !recorded)) return; + replaying = true; follow = false; + let at = from == null ? 0 : Math.max(0, order.indexOf(from)); + let first = true; + const next = () => { + if (!replaying) return; + if (at >= order.length) { replaying = false; follow = !recorded; setStatus(); document.dispatchEvent(new CustomEvent('git-sim:done')); return; } + const held = first ? wait : 0; + first = false; + show(order[at++], true, () => { + const hooks = window.GitSimLiveHooks || {}; + if (!hooks.hold) { setTimeout(next, 700); return; } + Promise.resolve().then(() => hooks.hold(at >= order.length)).catch(() => {}).then(() => setTimeout(next, 150)); + }, held); + }; + next(); + } + function stopReplay(){ replaying = false; showToken++; setStatus(); const hooks = window.GitSimLiveHooks || {}; if (hooks.stopped) hooks.stopped(); } + replayBtn.addEventListener('click', () => { if (replaying) stopReplay(); else replay(); }); + window.GitSimLive = { + play(from, wait){ if (recording) return; const order = items.map(i => i.index).filter(i => i !== 0); + if (from != null && order.includes(from)) { replay(from, wait); return; } + replay(current === order[order.length - 1] ? order[0] : current, wait); }, + stop(){ stopReplay(); }, + step(index){ if (recording || !items.some(i => i.index === index)) return; replaying = false; follow = false; + show(index, true, () => { setStatus(); document.dispatchEvent(new CustomEvent('git-sim:done')); }); }, + get playing(){ return replaying; }, + }; + document.addEventListener('keydown', e => { + const t = e.target; + if (t && (/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName) || t.isContentEditable)) return; + if (e.key === '[' || e.key === ']') { + const pos = items.findIndex(i => i.index === current); + const to = items[pos + (e.key === ']' ? 1 : -1)]; + if (to) { replaying = false; show(to.index, true); } + } + if (e.key === 'r' || e.key === 'R') replayBtn.click(); + }); + + if (recorded) { + followBtn.hidden = true; clearBtn.hidden = true; if (saveBtn) saveBtn.hidden = true; + follow = false; + (recorded.items || []).forEach(i => add(i, i.svg || stepSvg(i), false)); + const last = items[items.length - 1]; + if (recorded.start === 'first' && items.length) show(items[0].index, false, () => V.setProgress(0)); + else if (last) show(last.index, false); + } + setStatus(); +})(); + +(function(){ + const root = document.documentElement; + if (!root.dataset.player) return; + const $ = id => document.getElementById(id); + const chips = $('chips'), box = $('vpStep'); + if (!chips || !box) return; + const session = (() => { try { return JSON.parse(($('git-sim-session') || {}).textContent || 'null'); } catch (e) { return null; } })(); + const demo = !!(session && session.title); + const noun = demo ? 'Step' : 'Change'; + const icon = { + play: '', + pause: '', + }; + const head = document.createElement('div'); + head.className = 'vp-head'; + head.innerHTML = '

'; + const controls = $('controls'); + if (controls) controls.after(head); else $('bar').appendChild(head); + $('vpPlayIcon').innerHTML = icon.play; + + const labelOf = chip => { const c = chip.cloneNode(true); c.querySelectorAll('b, small').forEach(n => n.remove()); return c.textContent.trim(); }; + const timeOf = chip => { const t = chip.querySelector('small'); return t ? t.textContent.trim() : ''; }; + const steps = () => Array.from(chips.querySelectorAll('.chip')).filter(c => c.dataset.i !== '0'); + let paused = false; + let started = false, idle = false, lastStep = null; + const running = () => !!(window.GitSimLive && GitSimLive.playing); + const playing = () => running() && !paused; + function go(k){ + const s = steps()[k]; + if (!s) return; + const wasPlaying = playing(); + paused = false; + started = true; idle = false; + GitSimLive.stop(); + if (wasPlaying) GitSimLive.play(+s.dataset.i); else s.click(); + syncPlay(); + show(); + } + const STATES = {watching: 'Watching', paused: 'Paused', replaying: 'Replaying', recording: 'Recording', disconnected: 'Disconnected'}; + function pill(){ + const n = $('vpStepN'); + if (!session) { + const st = $('liveStatus'); + let s = (st && st.dataset.state) || ''; + if (s === 'replaying' && paused) s = 'paused'; + n.textContent = STATES[s] || ''; n.dataset.state = s; n.hidden = !STATES[s]; + return; + } + const all = steps(), cur = chips.querySelector('.chip.cur'), at = all.indexOf(cur); + n.textContent = !cur ? '' : at < 0 ? 'Watching' : noun + ' ' + (at + 1) + ' / ' + all.length; + n.hidden = !cur; + } + function show(){ + const all = steps(), cur = chips.querySelector('.chip.cur'); + const at = all.indexOf(cur); + box.hidden = false; + pill(); + const bar = $('vpStepBar'); + if (bar.dataset.n !== String(all.length)) { + bar.dataset.n = all.length; + bar.innerHTML = all.length ? '' : 'Waiting for the first change'; + all.forEach((chip, k) => { + const seg = document.createElement('button'), label = labelOf(chip), time = timeOf(chip); + seg.type = 'button'; seg.title = label + (time ? '\n' + time : ''); + seg.setAttribute('aria-label', noun + ' ' + (k + 1) + ': ' + label + (time ? ', ' + time : '')); + const meta = document.createElement('span'), cmd = document.createElement('span'); + meta.className = 'seg-meta'; meta.textContent = noun + ' ' + (k + 1) + (time ? ' · ' + time : ''); + cmd.className = 'seg-cmd'; cmd.textContent = label; + seg.append(meta, cmd); + seg.addEventListener('click', () => go(k)); + bar.appendChild(seg); + }); + } + Array.from(bar.children).forEach((seg, k) => { seg.classList.toggle('done', k < at); seg.classList.toggle('cur', k === at); }); + const curSeg = bar.children[at]; + if (curSeg && bar.classList.contains('labeled')) { + const sr = curSeg.getBoundingClientRect(), br = bar.getBoundingClientRect(); + if (sr.left < br.left) bar.scrollLeft += sr.left - br.left - 8; + else if (sr.right > br.right) bar.scrollLeft += sr.right - br.right + 8; + } + $('vpPrev').disabled = at <= 0; + $('vpNext').disabled = at >= all.length - 1; + const step = started && !idle ? at : -1; + if (step !== lastStep) { + lastStep = step; + document.dispatchEvent(new CustomEvent('git-sim:step', {detail: {step: step, steps: all.length}})); + } + } + new MutationObserver(show).observe(chips, {childList: true, subtree: true, attributes: true, attributeFilter: ['class']}); + + const DETAILS = 'git-sim:step-details'; + let details = root.dataset.compact ? false : (() => { try { return localStorage.getItem(DETAILS) !== '0'; } catch (e) { return true; } })(); + const labels = $('vpLabels'); + function applyDetails(){ + $('vpStepBar').classList.toggle('labeled', details); + box.classList.toggle('compact', !details); + if (!labels) return; + labels.setAttribute('aria-pressed', details ? 'true' : 'false'); + labels.textContent = details ? 'Hide details' : 'Show details'; + } + if (labels) labels.addEventListener('click', () => { + details = !details; + try { localStorage.setItem(DETAILS, details ? '1' : '0'); } catch (e) {} + applyDetails(); show(); + }); + applyDetails(); + + function syncPlay(){ + if (!started && running()) { started = true; show(); } + const on = playing(); + $('vpPlayIcon').innerHTML = on ? icon.pause : icon.play; + $('vpPlayText').textContent = on ? 'Pause' : 'Play'; + $('vpPlay').setAttribute('aria-label', on ? 'Pause' : 'Play'); + $('vpPlay').title = on ? 'Pause' : 'Play'; + pill(); + } + function togglePlay(){ + if (paused && running()) { paused = false; GitSimViewer.resumeOnce(); } + else if (running()) { paused = true; GitSimViewer.pauseOnce(); } + else if (root.dataset.compact && steps().length) { paused = false; started = true; idle = false; GitSimLive.play(+steps()[0].dataset.i, 500); } + else { paused = false; idle = false; GitSimLive.play(); } + syncPlay(); + show(); + } + document.addEventListener('git-sim:go', e => { + const s = steps()[(e.detail || {}).step]; + if (!s) return; + started = true; idle = false; paused = false; + GitSimLive.stop(); + GitSimLive.step(+s.dataset.i); + syncPlay(); show(); + }); + document.addEventListener('git-sim:done', () => { idle = true; show(); }); + $('vpPlay').addEventListener('click', togglePlay); + document.addEventListener('keydown', e => { + if (e.key === '[' || e.key === ']') { started = true; idle = false; setTimeout(show); } + }); + document.querySelectorAll('.vp-player button, .vp-watch').forEach(b => b.addEventListener('mousedown', e => e.preventDefault())); + $('vpPrev').addEventListener('click', () => { const all = steps(); go(all.indexOf(chips.querySelector('.chip.cur')) - 1); }); + $('vpNext').addEventListener('click', () => { const all = steps(); go(all.indexOf(chips.querySelector('.chip.cur')) + 1); }); + function syncStatus(){ + syncPlay(); pill(); + $('vpKicker').textContent = 'Workflow'; + $('vpName').textContent = session ? session.title : ''; + $('vpDot').className = ''; + } + document.addEventListener('git-sim:status', syncStatus); + syncStatus(); + show(); + + const stage = $('stage'); + function cap(){ + const svg = stage.querySelector('svg'); + const vb = svg && svg.viewBox && svg.viewBox.baseVal; + const commit = svg && svg.querySelector('[data-role="commit"]'); + if (!vb || !vb.width || !commit) { if (svg) svg.style.maxWidth = ''; return; } + const size = commit.getBBox().width; + if (!size) return; + const fixed = +root.dataset.commitPx || 0; + let width = vb.width * (fixed || 60) / size; + const framed = !!root.dataset.embedId; + const room = framed ? Infinity : window.innerHeight - (stage.getBoundingClientRect().top + window.scrollY) - 150; + if (room > 120 && !fixed) width = Math.max(Math.min(width, room * vb.width / vb.height), vb.width * 34 / size); + svg.style.maxWidth = Math.round(width) + 'px'; + svg.style.maxHeight = Math.round(width * vb.height / vb.width) + 'px'; + svg.style.marginLeft = svg.style.marginRight = 'auto'; + } + new MutationObserver(cap).observe(stage, {childList: true}); + window.addEventListener('resize', cap); + if (window.ResizeObserver) new ResizeObserver(() => cap()).observe(stage); + cap(); + + function reserveCorner(){ + const svg = stage.querySelector('svg'); + if (!svg) return; + const sb = svg.getBoundingClientRect(), stb = stage.getBoundingClientRect(); + if (!sb.width || !sb.height) return; + const texts = Array.from(document.querySelectorAll('script[type="image/svg+xml"][id^="git-sim-step-"]')).map(s => s.textContent); + if (session && session.items) session.items.forEach(it => { if (it.svg) texts.push(it.svg); }); + if (!texts.length) texts.push(svg.outerHTML); + const box = document.createElement('div'); + box.style.cssText = 'position:absolute;left:-10000px;top:0;visibility:hidden;width:' + sb.width + 'px'; + document.body.appendChild(box); + const size = 30, margin = 8, gap = 4; + const reach = margin + size + gap; + const offX = sb.left - stb.left, offY = sb.top - stb.top; + let need = 0; + texts.forEach(text => { + box.innerHTML = text; + const s = box.querySelector('svg'); + if (!s) return; + s.removeAttribute('id'); + s.style.cssText = 'display:block;width:' + sb.width + 'px;height:' + sb.height + 'px'; + const o = s.getBoundingClientRect(); + const vb = s.viewBox && s.viewBox.baseVal, k = vb && vb.width ? o.width / vb.width : 1; + s.querySelectorAll('[data-role]').forEach(el => { + const role = el.dataset.role; + if (role === 'background' || role === 'title' || role === 'commit-hit') return; + const b = el.getBoundingClientRect(); + if (!b.width && !b.height) return; + const dx = (parseFloat(el.dataset.dx) || 0) * k, dy = (parseFloat(el.dataset.dy) || 0) * k; + [[0, 0], [dx, dy]].forEach(([mx, my]) => { + if (b.left - o.left + offX + mx >= reach) return; + need = Math.max(need, b.bottom - o.top + offY + my + gap + size + margin - stb.height); + }); + }); + }); + box.remove(); + const pad = Math.max(0, Math.ceil(need)) + 'px'; + if (document.body.style.paddingBottom === pad) return; + document.body.style.paddingBottom = pad; + window.dispatchEvent(new Event('git-sim:layout')); + } + if (root.dataset.compact) { + const ready = document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve(); + ready.then(() => requestAnimationFrame(reserveCorner)); + window.addEventListener('resize', () => requestAnimationFrame(reserveCorner)); + } +})(); diff --git a/static/git-sim/cheat-sheet/cherry-pick.html b/static/git-sim/cheat-sheet/cherry-pick.html new file mode 100644 index 0000000000..212bd1b17b --- /dev/null +++ b/static/git-sim/cheat-sheet/cherry-pick.html @@ -0,0 +1,116 @@ + + + + + + git cherry-pick <commit> (git-sim) + + + + + +
+
+ + + + +
+
+ + + + +
+
+ +
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ + + + + + diff --git a/static/git-sim/cheat-sheet/fast-forward.html b/static/git-sim/cheat-sheet/fast-forward.html new file mode 100644 index 0000000000..7b80ba251b --- /dev/null +++ b/static/git-sim/cheat-sheet/fast-forward.html @@ -0,0 +1,146 @@ + + + + + + git merge banana (git-sim) + + + + + +
+
+ + + + +
+
+ + + + +
+
+ +
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ + + + + + + diff --git a/static/git-sim/cheat-sheet/merge.html b/static/git-sim/cheat-sheet/merge.html new file mode 100644 index 0000000000..206effee8b --- /dev/null +++ b/static/git-sim/cheat-sheet/merge.html @@ -0,0 +1,154 @@ + + + + + + git merge banana (git-sim) + + + + + +
+
+ + + + +
+
+ + + + +
+
+ +
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ + + + + + + diff --git a/static/git-sim/cheat-sheet/rebase.html b/static/git-sim/cheat-sheet/rebase.html new file mode 100644 index 0000000000..f046b24769 --- /dev/null +++ b/static/git-sim/cheat-sheet/rebase.html @@ -0,0 +1,159 @@ + + + + + + git rebase main (git-sim) + + + + + +
+
+ + + + +
+
+ + + + +
+
+ +
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ + + + + + + diff --git a/static/git-sim/cheat-sheet/squash.html b/static/git-sim/cheat-sheet/squash.html new file mode 100644 index 0000000000..f51a9afb9b --- /dev/null +++ b/static/git-sim/cheat-sheet/squash.html @@ -0,0 +1,249 @@ + + + + + + git merge --squash banana (git-sim) + + + + + +
+
+ + + + +
+
+ + + + +
+
+ +
+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ + + + + + + + diff --git a/static/js/git-sim-embed.js b/static/js/git-sim-embed.js new file mode 100644 index 0000000000..f98521d297 --- /dev/null +++ b/static/js/git-sim-embed.js @@ -0,0 +1,160 @@ +// Generated by git-sim 0.4.x (https://github.com/initialcommit-com/git-sim) +// from src/git_sim/render/embed.py. Do not edit: regenerate it with git-sim. +(function(){ + if (window.GitSimEmbed) return; + const VIEWER = null; + let counter = 0; + const frames = new Map(); + + function hostTheme(){ + const set = (document.documentElement.dataset.theme || '').toLowerCase(); + if (set === 'dark' || set === 'light') return set; + return window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + } + function themeFor(el){ + const asked = (el.dataset.theme || '').toLowerCase(); + if (asked === 'dark' || asked === 'light') return asked; + return hostTheme(); + } + function sendTheme(id){ + const f = frames.get(id); + if (f && f.iframe.contentWindow) f.iframe.contentWindow.postMessage({gitSimTheme: id, theme: f.theme}, '*'); + } + document.addEventListener('pointerdown', () => { + frames.forEach((f, id) => { if (f.iframe.contentWindow) f.iframe.contentWindow.postMessage({gitSimClose: id}, '*'); }); + }, true); + if (typeof MutationObserver === 'function') { + new MutationObserver(() => { + const now = hostTheme(); + frames.forEach((f, id) => { + const asked = (f.element.dataset.theme || '').toLowerCase(); + if (asked === 'dark' || asked === 'light') return; + if (f.theme !== now) { f.theme = now; sendTheme(id); } + }); + }).observe(document.documentElement, {attributes: true, attributeFilter: ['data-theme']}); + } + + const onHome = () => /(^|\.)initialcommit\.com$/i.test(location.hostname); + + function mount(el){ + if (el.dataset.gitSimMounted) return; + el.dataset.gitSimMounted = '1'; + const src = el.dataset.src, graph = el.dataset.graph; + if (!src && !graph) { el.textContent = 'git-sim embed: data-graph or data-src is missing'; return; } + if (graph && typeof DecompressionStream === 'undefined') { el.textContent = 'git-sim embed: this browser cannot unpack the graph (it needs DecompressionStream)'; return; } + const saved = !graph && /\.html?(\?|#|$)/i.test(src); + if (!saved && !VIEWER) { el.textContent = 'git-sim embed: this copy of the script shows saved pages only'; return; } + const id = 'gs' + (++counter); + const iframe = document.createElement('iframe'); + iframe.setAttribute('title', el.dataset.title ? 'git-sim: ' + el.dataset.title : 'git-sim graph'); + const px = v => v && (/^\d+(\.\d+)?$/.test(v) ? v + 'px' : v); + const start = fallback => el.dataset.height || px(el.dataset.heightHint) || fallback; + iframe.style.cssText = 'display:block;width:100%;border:0;border-radius:12px;background:transparent;height:' + start('320px'); + iframe.setAttribute('loading', 'lazy'); + const theme = themeFor(el); + const controls = (el.dataset.controls || 'full').toLowerCase() === 'compact' ? 'compact' : 'full'; + if (!graph && /\.html?(\?|#|$)/i.test(src)) { + iframe.style.height = start('560px'); + frames.set(id, {iframe, element: el, theme, svg: null, ready: true}); + el.replaceChildren(iframe); + const withCredit = !onHome() && (el.dataset.credit || '').toLowerCase() !== 'none'; + const base = (() => { try { return new URL(src, document.baseURI).href; } catch (e) { return ''; } })(); + fetch(src, {credentials: 'same-origin'}).then(r => { if (!r.ok) throw new Error(r.status + ' fetching ' + src); return r.text(); }) + .then(html => { iframe.srcdoc = fitted(html, id, controls, withCredit, base); }) + .catch(err => { el.textContent = 'git-sim embed: ' + err.message; }); + return; + } + } + + const PAGE_CSS = 'html,body{overflow:hidden;min-height:0}body{position:relative}#learn{display:none!important}' + + '[data-controls="compact"] #brand,[data-controls="compact"] #bar .right a,[data-controls="compact"] .vp-rec,[data-controls="compact"] .vp-sep,[data-controls="compact"] .vp-step-top{display:none!important}' + + '[data-controls="compact"] .vp-step-bar.labeled button{max-width:340px}' + + '[data-controls="compact"] #helpMenu .help-credit a{display:inline-block!important}' + + '.gs-made{position:absolute;right:14px;bottom:12px;margin:0;font:13px/1.5 var(--font);color:var(--muted);white-space:nowrap}' + + '.gs-made a{display:inline-block;margin-left:4px;padding:2px 10px;border-radius:999px;border:1px solid #c7d2fe;background:#eef2ff;color:#3730a3;font-weight:700;text-decoration:none}' + + '.gs-made a:hover{background:#4f46e5;border-color:#4f46e5;color:#fff}' + + '[data-theme="dark"] .gs-made a{border-color:rgba(129,140,248,.38);background:rgba(99,102,241,.16);color:#c7d2fe}' + + '[data-theme="dark"] .gs-made a:hover{background:#818cf8;border-color:#818cf8;color:#fff}'; + const PAGE_HTML = '

made with git-sim

'; + const PAGE_JS = `(function(){ + var root = document.documentElement, id = root.dataset.embedId; + var send = function(){ parent.postMessage({gitSimEmbed: id, height: document.body.scrollHeight}, '*'); }; + window.addEventListener('load', function(){ send(); parent.postMessage({gitSimEmbedReady: id}, '*'); }); + window.addEventListener('git-sim:layout', send); + document.addEventListener('git-sim:step', function(e){ var d = e.detail || {}; parent.postMessage({gitSimStep: id, step: d.step, steps: d.steps}, '*'); }); + if (window.ResizeObserver) new ResizeObserver(send).observe(document.body); + var closeMenus = function(){ if (document.querySelector('#shareMenu:not([hidden]), #helpMenu:not([hidden])')) document.body.click(); }; + window.addEventListener('blur', closeMenus); + window.addEventListener('message', function(e){ + if (e.source !== window.parent) return; + var m = e.data || {}; + if (m.gitSimClose === id) { closeMenus(); return; } + if (m.gitSimGo === id && typeof m.step === 'number') { document.dispatchEvent(new CustomEvent('git-sim:go', {detail: {step: m.step}})); return; } + if (m.gitSimTheme !== id || !m.theme) return; + root.dataset.theme = m.theme; + try { if (window.GitSimViewer && GitSimViewer.setTheme) GitSimViewer.setTheme(m.theme); } catch (err) {} + send(); + }); + var help = document.getElementById('helpMenu'); + if (help && root.dataset.controls === 'compact') { + var keep = {hover: 1, click: 1, share: 1}; + help.querySelectorAll('li').forEach(function(li){ var k = li.querySelector('kbd'); if (!k || !keep[k.textContent.trim()]) li.remove(); }); + help.querySelectorAll('.help-more').forEach(function(p){ p.remove(); }); + } + })();`; + function fitted(html, id, controls, withCredit, base){ + const head = ''; + const tail = (withCredit ? PAGE_HTML : '') + '