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 @@
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>}}
+
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>}}
+
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>}}
+
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>}}
+
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>}}
+
.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 `'; + 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 @@ + + + + + +
made with git-sim
+made with git-sim
+made with git-sim
+made with git-sim
+made with git-sim
+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 : '') + '