|
44 | 44 | #search:focus { border-color: var(--accent); } |
45 | 45 | .btn { display: inline-flex; align-items: center; gap: 6px; height: 29px; border: 1px solid var(--line); border-radius: 6px; padding: 0 10px; background: var(--bg); cursor: pointer; white-space: nowrap; } |
46 | 46 | .btn:hover { background: var(--hover); } |
47 | | - .seg { display: inline-flex; height: 29px; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; background: var(--bg); } |
48 | | - .seg button { border: 0; padding: 0 10px; background: transparent; cursor: pointer; } |
49 | | - .seg span { display: flex; align-items: center; padding: 0 9px; color: var(--muted); border-right: 1px solid var(--line); } |
50 | | - .seg button + button { border-left: 1px solid var(--line); } |
51 | | - .seg button:hover { background: var(--hover); } |
| 47 | + .menu { position: absolute; right: 10px; top: 55px; z-index: 6; min-width: 190px; padding: 4px; display: grid; |
| 48 | + background: var(--panel); border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 8px 28px rgba(0,0,0,.12); } |
| 49 | + .menu[hidden] { display: none; } |
| 50 | + .menu button { border: 0; border-radius: 5px; padding: 7px 10px; background: transparent; cursor: pointer; text-align: left; color: var(--fg); } |
| 51 | + .menu button:hover { background: var(--hover); } |
| 52 | + .menu button small { color: var(--muted); margin-left: 6px; } |
| 53 | + #json-dlg { width: min(720px, calc(100vw - 40px)); padding: 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); color: var(--fg); } |
| 54 | + #json-dlg::backdrop { background: rgba(0,0,0,.35); } |
| 55 | + #json-dlg form { display: grid; gap: 10px; } |
| 56 | + #json-dlg [hidden] { display: none; } |
| 57 | + #json-dlg strong { font-size: 14px; } |
| 58 | + #json-dlg textarea { height: min(60vh, 520px); resize: vertical; border: 1px solid var(--line); border-radius: 6px; padding: 8px; background: var(--bg); color: var(--fg); |
| 59 | + font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; } |
| 60 | + #json-dlg .row { display: flex; align-items: center; gap: 8px; } |
| 61 | + #json-dlg .row span { flex: 1; color: var(--muted); font-size: 12px; } |
| 62 | + #import-btn { width: 28px; height: 28px; border: 0; border-radius: 6px; background: transparent; color: var(--muted); cursor: pointer; font-size: 16px; font-weight: 400; } |
| 63 | + #import-btn:hover { background: var(--hover); color: var(--fg); } |
52 | 64 | .sw { display: inline-block; width: 10px; height: 10px; border-radius: 50%; box-shadow: 0 0 0 1px rgba(0,0,0,.15) inset; margin-right: -3px; } |
53 | 65 | .sws { display: inline-flex; padding-right: 3px; } |
54 | 66 |
|
|
143 | 155 | <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.2" fill="none" stroke="currentColor" stroke-width="1.6"/></svg> |
144 | 156 | Giotto |
145 | 157 | <button id="theme-btn" title="Light, dark, or follow the system"></button> |
| 158 | + <button id="import-btn" title="Import a diagram from its JSON">+</button> |
146 | 159 | </div> |
147 | 160 | <div id="kinds" role="tablist" aria-label="Show"><button data-kind="all">All</button><button data-kind="diagram">Diagrams</button><button data-kind="page">Images</button></div> |
148 | 161 | <nav id="list"></nav> |
|
154 | 167 | <input id="search" type="search" placeholder="Search ⌘F" aria-label="Search this diagram" /> |
155 | 168 | <button id="history-btn" class="btn" title="Every change is a version; look back and restore">History</button> |
156 | 169 | <button id="style-btn" class="btn" aria-haspopup="true"></button> |
157 | | - <div class="seg"><span>Export</span><button data-export="svg">SVG</button><button data-export="html" hidden>HTML</button><button data-export="png">PNG</button><button data-export="json">JSON</button><button id="anim-btn" hidden title="Export the scenes as a video or animated SVG">Animation</button></div> |
| 170 | + <button id="export-btn" class="btn" aria-haspopup="true">Export ▾</button> |
158 | 171 | </header> |
| 172 | + <div id="export-menu" class="menu" hidden> |
| 173 | + <button data-export="svg">SVG</button><button data-export="html" hidden>HTML</button><button data-export="png">PNG</button> |
| 174 | + <button data-export="json">JSON <small>copy, to import elsewhere</small></button> |
| 175 | + <button id="anim-btn" hidden>Animation… <small>MP4 or animated SVG</small></button> |
| 176 | + </div> |
| 177 | + <dialog id="json-dlg"><form method="dialog"> |
| 178 | + <strong id="json-title"></strong> |
| 179 | + <textarea id="json-text" spellcheck="false" aria-label="Diagram JSON"></textarea> |
| 180 | + <div class="row"><span id="json-status"></span><button id="json-copy" class="btn" type="button">Copy</button><button id="json-import" class="btn" type="button">Import</button><button class="btn">Close</button></div> |
| 181 | + </form></dialog> |
159 | 182 | <div id="scenes" hidden></div> |
160 | 183 | <div id="gallery" hidden></div> |
161 | 184 | <div id="history" class="panel" hidden></div> |
|
886 | 909 | URL.revokeObjectURL(a.href); |
887 | 910 | } |
888 | 911 |
|
| 912 | + $('export-btn').onclick = () => ($('export-menu').hidden = !$('export-menu').hidden); |
| 913 | + document.addEventListener('pointerdown', (ev) => { |
| 914 | + if (!ev.target.closest('#export-menu, #export-btn')) $('export-menu').hidden = true; |
| 915 | + }); |
| 916 | + |
| 917 | + // JSON in a dialog: copy it out of one Giotto, paste it into another's Import (the + by the logo). |
| 918 | + function jsonDialog(text) { |
| 919 | + const importing = text === undefined; |
| 920 | + $('json-title').textContent = importing ? 'Import a diagram' : `${doc.title || current} as JSON`; |
| 921 | + $('json-text').value = importing ? '' : JSON.stringify(JSON.parse(text), null, 2); |
| 922 | + $('json-text').readOnly = !importing; |
| 923 | + $('json-text').placeholder = importing ? 'Paste a diagram\'s JSON (Export → JSON in any Giotto)' : ''; |
| 924 | + $('json-status').textContent = ''; |
| 925 | + $('json-copy').hidden = importing; |
| 926 | + $('json-import').hidden = !importing; |
| 927 | + $('json-dlg').showModal(); |
| 928 | + importing ? $('json-text').focus() : ($('json-text').select(), ($('json-text').scrollTop = 0)); |
| 929 | + } |
| 930 | + $('json-copy').onclick = async () => { |
| 931 | + await navigator.clipboard.writeText($('json-text').value).catch(() => $('json-text').select()); |
| 932 | + $('json-status').textContent = 'Copied.'; |
| 933 | + }; |
| 934 | + $('import-btn').onclick = () => jsonDialog(); |
| 935 | + $('json-import').onclick = async () => { |
| 936 | + let parsed; |
| 937 | + try { parsed = JSON.parse($('json-text').value); } catch (e) { return void ($('json-status').textContent = `Not valid JSON: ${e.message}`); } |
| 938 | + const res = await fetch('/api/doc', { method: 'POST', body: JSON.stringify(parsed) }); |
| 939 | + const out = await res.json(); |
| 940 | + if (!res.ok) return void ($('json-status').textContent = out.error); |
| 941 | + $('json-dlg').close(); |
| 942 | + await refreshList(); |
| 943 | + await open(out.id); |
| 944 | + }; |
| 945 | + |
889 | 946 | document.querySelectorAll('[data-export]').forEach((btn) => btn.addEventListener('click', async () => { |
| 947 | + $('export-menu').hidden = true; |
890 | 948 | if (!doc) return; |
891 | 949 | const f = btn.dataset.export; |
| 950 | + if (f === 'json') return jsonDialog(base ?? JSON.stringify(doc)); |
892 | 951 | if (doc.html !== undefined) { |
893 | 952 | // A page: PNG drawn by Chrome on the server, or the HTML as one file. |
894 | 953 | if (f === 'html') download(`${fileName()}.html`, pageHtml(doc, styles[styleId], { dark: isDark() }), 'text/html'); |
895 | | - if (f === 'json') download(`${fileName()}.json`, base, 'application/json'); |
896 | 954 | if (f !== 'png') return; |
897 | 955 | const res = await fetch(`/api/page.png?id=${current}&dark=${isDark() ? 1 : 0}`); |
898 | 956 | return res.ok ? download(`${fileName()}.png`, await res.blob()) : toast((await res.json()).error); |
|
904 | 962 | } |
905 | 963 | if (f === 'svg') download(`${fileName()}.svg`, toSvg(doc, S), 'image/svg+xml'); |
906 | 964 | if (f === 'png') download(`${fileName()}.png`, await toPng(toSvg(doc, S))); |
907 | | - if (f === 'json') download(`${fileName()}.json`, base, 'application/json'); |
908 | 965 | })); |
909 | 966 |
|
910 | 967 | // ---- scenes: play a diagram's stories (tabs, play/pause, 1x/2x) ---- |
|
999 | 1056 | })); |
1000 | 1057 | $('anim-btn').onclick = (ev) => { |
1001 | 1058 | ev.stopPropagation(); |
1002 | | - $('anim').hidden ? showAnimPanel() : ($('anim').hidden = true); |
| 1059 | + $('export-menu').hidden = true; |
| 1060 | + showAnimPanel(); |
1003 | 1061 | }; |
1004 | 1062 | document.addEventListener('pointerdown', (ev) => { |
1005 | 1063 | if (!ev.target.closest('#anim, #anim-btn')) $('anim').hidden = true; |
|
0 commit comments