commit 2952e539aa8c5d21ddc031e0ea0ba22948ac9604 Author: Your Name Date: Fri Oct 9 22:39:37 2026 +0200 feat: Front-matter properties editor and inline math bug fix diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..b62b068 --- /dev/null +++ b/src/app.css @@ -0,0 +1,760 @@ +/* Two floating islands (sidebar + page) on a neutral, finely grained ground. Dark first; light is its daytime twin. + Fonts are bundled (@fontsource), so the app never touches the network. */ +:root{ + --mark:rgba(250,204,21,.26); /* ==highlight== */ + --fs:16px; /* page text size (Settings) */ + --page-w:740px; /* page width (Settings) */ + --side:clamp(20px,3.5vw,48px); /* page edge padding for full-width pages */ + --ground:#0e0e0f; --island:#161617; --island-2:#1c1c1e; --raise:#222224; + --line:rgba(255,255,255,.07); --line-2:rgba(255,255,255,.13); + --hover:rgba(255,255,255,.055); --press:rgba(255,255,255,.095); + --ink:#ececed; --ink-2:#a1a1a6; --ink-3:#6c6c71; + --blush:#e4ada5; --blush-ink:#2a1714; --blush-soft:rgba(228,173,165,.13); --blush-line:rgba(228,173,165,.4); + --ok:#93b98d; --danger:#ea9188; --scrim:rgba(0,0,0,.5); + --code-bg:#111112; --tk:#e4ada5; --ts:#c5cf98; --tc:#6c6c71; --tn:#e8bb83; --tf:#b7c3e2; + --shadow:0 24px 60px -16px rgba(0,0,0,.65),0 2px 10px rgba(0,0,0,.35); + --grain:.05; + --f-ui:'Inter Variable','Inter',ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif; + --f-mono:'JetBrains Mono',ui-monospace,'SF Mono',Menlo,Consolas,monospace; + --ease:cubic-bezier(.2,.8,.2,1); --spring:cubic-bezier(.34,1.45,.64,1); + color-scheme:dark; +} +:root[data-theme="light"]{ + --mark:rgba(250,204,21,.42); + --ground:#e7e7e9; --island:#fbfbfb; --island-2:#f3f3f4; --raise:#ffffff; + --line:rgba(0,0,0,.075); --line-2:rgba(0,0,0,.14); + --hover:rgba(0,0,0,.045); --press:rgba(0,0,0,.085); + --ink:#1c1c1e; --ink-2:#5c5c61; --ink-3:#9a9a9f; + --blush:#b8695f; --blush-ink:#fff8f5; --blush-soft:rgba(184,105,95,.11); --blush-line:rgba(184,105,95,.4); + --ok:#5f8f58; --danger:#b4493e; --scrim:rgba(20,20,22,.22); + --code-bg:#f2f2f3; --tk:#a8564c; --ts:#5c7440; --tc:#9a9a9f; --tn:#a4681d; --tf:#46609b; + --shadow:0 24px 60px -18px rgba(0,0,0,.25),0 2px 10px rgba(0,0,0,.07); + --grain:.07; + color-scheme:light; +} +*{box-sizing:border-box} +/* Scrollbars: thin, no arrows, and only visible while the scrolling area is hovered. */ +::-webkit-scrollbar{width:10px;height:10px} +::-webkit-scrollbar-track,::-webkit-scrollbar-corner{background:transparent} +::-webkit-scrollbar-button{display:none} +::-webkit-scrollbar-thumb{border-radius:99px;border:3px solid transparent;background:transparent padding-box;transition:background-color .2s} +:hover::-webkit-scrollbar-thumb{background-color:var(--line-2)} +::-webkit-scrollbar-thumb:hover{background-color:var(--ink-3)} +::-webkit-scrollbar-thumb:active{background-color:var(--ink-2)} +html,body{height:100%} +body{margin:0;background:var(--ground);color:var(--ink);font:13.5px/1.5 var(--f-ui);-webkit-font-smoothing:antialiased;overflow:hidden;user-select:none} +.c,.c2,.title,input,pre,.exp-out,.pv{user-select:text} +body::after{content:"";position:fixed;inset:0;pointer-events:none;z-index:200;opacity:var(--grain);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .5 0 0 0 0 .5 0 0 0 0 .5 0 0 0 .9 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")} +::selection{background:rgba(228,173,165,.32)} +button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer} +:focus-visible{outline:2px solid var(--blush-line);outline-offset:2px;border-radius:5px} +.i{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;flex:none} +.i.fat{stroke-width:3.2} +kbd{font:500 10.5px/1 var(--f-mono);padding:3px 5px;border-radius:4px;background:var(--hover);box-shadow:inset 0 -1px 0 var(--line-2);color:var(--ink-2)} + +/* shell */ +.app{height:100%;display:grid;grid-template-columns:268px minmax(0,1fr);gap:10px;padding:10px;transition:grid-template-columns .5s var(--ease),gap .5s var(--ease)} +.app.collapsed{grid-template-columns:0px minmax(0,1fr);gap:0} +.island{background:var(--island);border-radius:12px;box-shadow:0 0 0 1px var(--line);min-width:0;min-height:0} +.sidebar{overflow:hidden;transition:opacity .35s var(--ease),transform .45s var(--ease)} +.app.collapsed .sidebar{opacity:0;pointer-events:none} +.sb-inner{width:268px;height:100%;display:flex;flex-direction:column;padding:10px;gap:2px} +.btn-icon{width:30px;height:30px;display:grid;place-items:center;border-radius:6px;color:var(--ink-2);transition:background .15s,color .15s,transform .2s var(--spring);flex:none} +.btn-icon:hover{background:var(--hover);color:var(--ink)} +.btn-icon:active{transform:scale(.9)} +.ws{display:flex;align-items:center;gap:10px;padding:6px 4px 10px 6px} +.ws-mark{width:30px;height:30px;border-radius:7px;background:var(--blush);color:var(--blush-ink);display:grid;place-items:center;font-weight:700;font-size:14px;flex:none;transition:transform .4s var(--spring)} +.ws:hover .ws-mark{transform:rotate(-8deg)} +.ws-txt{display:flex;flex-direction:column;min-width:0;flex:1;line-height:1.25} +.ws-txt strong{font-weight:600;font-size:14px} +.ws-txt span{font-size:11.5px;color:var(--ink-3);font-family:var(--f-mono)} +.qa{display:flex;align-items:center;gap:10px;height:32px;padding:0 9px;border-radius:6px;color:var(--ink-2);transition:background .15s,color .15s,transform .15s var(--ease);text-align:left} +.qa:hover{background:var(--hover);color:var(--ink)} +.qa:active{transform:scale(.98)} +.qa kbd{margin-left:auto} +.sec{display:flex;align-items:center;justify-content:space-between;padding:16px 8px 6px 9px;font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3)} +.sec .btn-icon{width:22px;height:22px;border-radius:5px} +.sec .btn-icon .i{width:14px;height:14px} +.tree{flex:1;overflow:auto;min-height:60px;margin:0 -4px;padding:0 4px} +.row{display:flex;align-items:center;gap:6px;height:31px;padding:0 5px 0 calc(5px + var(--d,0)*14px);border-radius:6px;color:var(--ink-2);cursor:pointer;transition:background .18s,color .18s,transform .18s var(--ease);user-select:none} +.row:hover{background:var(--hover);color:var(--ink)} +.row:active{transform:scale(.985)} +.row.active{background:var(--blush-soft);color:var(--ink)} +.row.active .row-ico{color:var(--blush)} +.row .name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.row-ico{display:grid;place-items:center;color:var(--ink-3);transition:color .2s} +.chev{width:20px;height:20px;display:grid;place-items:center;border-radius:5px;color:var(--ink-3);transition:transform .32s var(--spring),background .15s} +.chev:hover{background:var(--press)} +.chev .i{width:12px;height:12px;stroke-width:2.2} +.chev.open{transform:rotate(90deg)} +.chev.leaf{visibility:hidden} +.row-add{width:22px;height:22px;display:grid;place-items:center;border-radius:5px;color:var(--ink-3);opacity:0;transform:scale(.7);transition:opacity .15s,transform .2s var(--spring),background .15s} +.row-add .i{width:14px;height:14px} +.row:hover .row-add{opacity:1;transform:none} +.row-add:hover{background:var(--press);color:var(--ink)} +.kids{display:grid;grid-template-rows:0fr;transition:grid-template-rows .38s var(--ease)} +.kids.open{grid-template-rows:1fr} +.kids-in{overflow:hidden;min-height:0} +.kids-in>.node{opacity:0;transform:translateY(-5px);transition:opacity .25s,transform .35s var(--ease)} +.kids.open>.kids-in>.node{opacity:1;transform:none} +.kids.open>.kids-in>.node:nth-child(2){transition-delay:.04s} +.kids.open>.kids-in>.node:nth-child(3){transition-delay:.08s} +.kids.open>.kids-in>.node:nth-child(n+4){transition-delay:.12s} +.row.fresh{animation:fresh .7s var(--ease)} +.row.gone{opacity:0;transform:translateX(-12px);transition:opacity .2s,transform .22s var(--ease)} +.row.flat{animation:rise .38s var(--ease) both;animation-delay:calc(var(--i,0)*40ms)} +.filter-head{display:flex;align-items:center;justify-content:space-between;padding:4px 6px 8px 9px;color:var(--ink-2);font-size:12.5px;animation:rise .3s var(--ease)} +.filter-head b{color:var(--blush);font-weight:500} +.filter-head button{color:var(--ink-3);font-size:12px;padding:2px 6px;border-radius:5px} +.filter-head button:hover{background:var(--hover);color:var(--ink)} +.tags{display:flex;flex-wrap:wrap;gap:5px;padding:2px 6px 8px} +.tag{display:inline-flex;align-items:center;gap:3px;height:26px;padding:0 9px;border-radius:99px;background:var(--hover);color:var(--ink-2);font-size:12.5px;transition:background .18s,color .18s,transform .25s var(--spring)} +.tag span{color:var(--ink-3)} +.tag em{font-style:normal;font-size:11px;color:var(--ink-3);margin-left:3px;font-variant-numeric:tabular-nums} +.tag:hover{background:var(--press);color:var(--ink);transform:translateY(-1px)} +.tag.on{background:var(--blush);color:var(--blush-ink)} +.tag.on span,.tag.on em{color:inherit;opacity:.7} +.sb-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 6px 4px 9px;margin-top:6px;border-top:1px solid var(--line);color:var(--ink-3);font-size:12px} +.sb-foot span{display:flex;align-items:center;gap:7px} +.sb-foot .dot{width:6px;height:6px;border-radius:50%;background:var(--ok)} +.knob{position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:var(--blush);color:var(--blush-ink);display:grid;place-items:center;transition:transform .5s var(--spring)} +.knob .i{width:12px;height:12px;stroke-width:2.2;position:absolute;transition:opacity .3s,transform .5s var(--spring)} +.knob .sun{opacity:0;transform:rotate(-90deg) scale(.5)} + +/* main island */ +.main{display:flex;flex-direction:column;overflow:hidden;position:relative} +.topbar{display:flex;align-items:center;gap:6px;height:50px;padding:0 10px 0 12px;flex:none} +.menu-btn{display:none} +.app.collapsed .menu-btn{display:grid;animation:pop .3s var(--spring)} +.crumbs{display:flex;align-items:center;gap:1px;min-width:0;flex:1;overflow:hidden;color:var(--ink-2);font-size:13px} +.crumb{display:flex;align-items:center;gap:6px;padding:4px 7px;border-radius:5px;white-space:nowrap;min-width:0;transition:background .15s,color .15s} +.crumb span{overflow:hidden;text-overflow:ellipsis} +.crumb:hover{background:var(--hover);color:var(--ink)} +.crumb:last-child{color:var(--ink)} +.crumb .i{width:14px;height:14px;color:var(--ink-3)} +.crumbs .sep{color:var(--ink-3);padding:0 1px} +.status{display:flex;align-items:center;gap:7px;color:var(--ink-3);font-size:12.5px;padding:0 6px;white-space:nowrap;opacity:0;transition:opacity 0.5s} +.status.visible{opacity:1;transition:opacity 0.2s} +.status .dot{width:6px;height:6px;border-radius:50%;background:var(--ok);transition:background .3s} +.status.saving .dot{background:var(--blush);animation:pulse 1s ease-in-out infinite} +.wc{color:var(--ink-3);font-size:12.5px;padding:0 6px;white-space:nowrap;font-variant-numeric:tabular-nums} +.scroller{flex:1;overflow:auto;overflow-x:hidden} +.cover{position:relative;height:clamp(110px,21vh,190px);margin:0 10px;border-radius:10px;overflow:hidden;background: + radial-gradient(110% 150% at 12% 0%,color-mix(in oklab,var(--c1) 62%,transparent),transparent 62%), + radial-gradient(90% 130% at 92% 110%,color-mix(in oklab,var(--c2) 55%,transparent),transparent 66%), + var(--island-2)} +.cover::after{content:"";position:absolute;inset:0;background:inherit;filter:blur(30px);opacity:.5;animation:drift 18s ease-in-out infinite alternate;pointer-events:none} +.cover.enter{animation:coverIn .7s var(--ease)} +.cover-btn{position:absolute;right:12px;bottom:12px;z-index:1;display:flex;align-items:center;gap:6px;height:28px;padding:0 10px;border-radius:6px;font-size:12px;color:var(--ink);background:color-mix(in oklab,var(--island) 72%,transparent);backdrop-filter:blur(8px);opacity:0;transform:translateY(4px);transition:opacity .2s,transform .25s var(--ease),background .15s} +.cover:hover .cover-btn,.cover-btn:focus-visible{opacity:1;transform:none} +.cover-btn .i{width:14px;height:14px} +.page{max-width:var(--page-w);margin:0 auto;padding:0 clamp(20px,6vw,72px)} +.page.enter .head{animation:rise .5s var(--ease) both} +.p-icon{position:relative;z-index:2;width:76px;height:76px;margin-top:-42px;border-radius:14px;background:var(--island);display:grid;place-items:center;color:var(--blush);box-shadow:0 0 0 1px var(--line-2),var(--shadow);transition:transform .45s var(--spring)} +.p-icon:hover{transform:rotate(-7deg) scale(1.06)} +.p-icon[aria-expanded="true"]{transform:none;box-shadow:0 0 0 1.5px var(--blush-line),var(--shadow)} +.p-icon.spin{animation:spin .6s var(--spring)} +.p-icon .i{width:34px;height:34px;stroke-width:1.35} +.i.emo{display:inline-grid;place-items:center;font-size:13px;line-height:1;stroke:none;font-family:'Noto Color Emoji','Apple Color Emoji','Segoe UI Emoji',sans-serif} +.p-icon .i.emo{font-size:38px} +.pop.ipick{width:340px;padding:8px;border-radius:10px;z-index:75;transform-origin:top left} +.ip-top{display:flex;align-items:center;gap:2px;margin-bottom:8px} +.ip-tabs{display:flex;gap:2px;flex:1;padding:3px;border-radius:7px;background:var(--hover)} +.ip-tab{flex:1;height:26px;border-radius:5px;font-size:12.5px;font-weight:550;color:var(--ink-3);transition:background .15s,color .15s} +.ip-tab.on{background:var(--raise);color:var(--ink);box-shadow:0 1px 3px rgba(0,0,0,.18)} +.ip-tab:not(.on):hover{color:var(--ink)} +.ip-top .btn-icon{width:30px;height:30px} +.ip-search{display:flex;align-items:center;gap:8px;height:34px;padding:0 10px;margin-bottom:8px;border-radius:7px;background:var(--hover);color:var(--ink-3)} +.ip-search input{flex:1;min-width:0;border:0;outline:0;background:none;color:var(--ink);font:13.5px var(--f-ui)} +.ip-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:2px;max-height:252px;overflow:auto;outline:none} +.ip-cell{aspect-ratio:1;display:grid;place-items:center;border-radius:6px;color:var(--ink-2);transition:background .12s,color .12s,transform .15s var(--spring)} +.ip-cell:hover,.ip-cell:focus-visible{background:var(--hover);color:var(--ink);transform:scale(1.08)} +.ip-cell.on{background:var(--blush-soft);color:var(--blush)} +.ip-cell .i{width:19px;height:19px} +.ip-emo{font-size:21px;line-height:1;font-family:'Noto Color Emoji','Apple Color Emoji','Segoe UI Emoji',sans-serif} +.ip-none{grid-column:1/-1;margin:0;padding:18px 6px;text-align:center;color:var(--ink-3);font-size:13px} +.title{font-size:clamp(calc(var(--fs) * 1.9),4.6vw,calc(var(--fs) * 2.6));font-weight:700;letter-spacing:-.028em;line-height:1.15;margin:20px 0 14px;outline:none;text-wrap:balance;word-break:break-word} +.title:empty::before{content:attr(data-ph);color:var(--ink-3)} +.props{display:grid;gap:2px;padding-bottom:16px;margin-bottom:10px;border-bottom:1px solid var(--line)} +.prop{display:grid;grid-template-columns:120px minmax(0,1fr);align-items:center;min-height:32px;gap:8px} +.pk{display:flex;align-items:center;gap:8px;color:var(--ink-3);font-size:13px} +.pk .i{width:15px;height:15px} +.pv{color:var(--ink-2);font-size:13px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.pv.mono{font-family:var(--f-mono);font-size:12px} +.tagv{display:flex;flex-wrap:wrap;gap:5px;white-space:normal;overflow:visible} +.chip{display:inline-flex;align-items:center;gap:2px;height:24px;padding:0 4px 0 9px;border-radius:99px;background:var(--blush-soft);color:var(--ink);font-size:12.5px;transition:opacity .18s,transform .2s var(--ease)} +.chip.in{animation:chipIn .4s var(--spring)} +.chip.out{opacity:0;transform:scale(.7)} +.chip-x{width:18px;height:18px;display:grid;place-items:center;border-radius:50%;color:var(--ink-3);opacity:.6;transition:.15s} +.chip-x:hover{opacity:1;background:var(--hover);color:var(--ink)} +.chip-x .i{width:11px;height:11px;stroke-width:2.2} +.chip-add{display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 9px 0 6px;border-radius:99px;color:var(--ink-3);font-size:12.5px;transition:.15s} +.chip-add:hover{background:var(--hover);color:var(--ink)} +.chip-add .i{width:13px;height:13px} +.chip-in{height:24px;width:110px;border:0;outline:0;border-radius:99px;padding:0 10px;background:var(--hover);box-shadow:inset 0 0 0 1px var(--blush-line);color:var(--ink);font:12.5px var(--f-ui);animation:chipIn .3s var(--spring)} + +/* blocks */ +.blocks{position:relative;padding-top:4px} +.block{position:relative;display:flex;align-items:flex-start;gap:6px;margin:1px 0;border-radius:6px} +.block.stagger{animation:rise .5s var(--ease) both;animation-delay:calc(var(--i,0)*26ms + 90ms)} +.block.enter-b{animation:blockIn .3s var(--ease)} +.block.morph{animation:morph .36s var(--spring)} +.block.leaving{opacity:0;transform:translateX(-8px);transition:opacity .16s,transform .18s var(--ease)} +.block.dragging{opacity:.3} +.block.drag-ghost{position:fixed;z-index:80;margin:0;pointer-events:none;background:var(--raise);padding:2px 8px;translate:-8px 0;border-radius:7px;box-shadow:0 0 0 1px var(--line-2),var(--shadow);opacity:.92;rotate:-.6deg} +.drag-ghost>.gutter{display:none} +body.block-dragging,body.block-dragging *{cursor:grabbing!important;user-select:none!important} +.gutter{position:absolute;left:-54px;top:var(--gt,4px);display:flex;opacity:0;transform:translateX(4px);transition:opacity .18s,transform .22s var(--ease)} +.block:hover>.gutter{opacity:1;transform:none} +.gutter button{width:24px;height:24px;display:grid;place-items:center;border-radius:5px;color:var(--ink-3);transition:background .15s,color .15s} +.gutter button:hover{background:var(--hover);color:var(--ink)} +.g-drag{cursor:grab} +.g-drag:active{cursor:grabbing} +.gutter .i{width:15px;height:15px} +.c,.c2{flex:1;min-width:0;outline:none;font-size:var(--fs);line-height:1.7;padding:1px 2px;white-space:pre-wrap;word-wrap:break-word;caret-color:var(--blush)} +.c:empty::before,.c2:empty::before{content:attr(data-ph);color:var(--ink-3);pointer-events:none} +.b-p .c:empty:not(:focus)::before{content:""} +.c code,.c2 code{font:500 .84em var(--f-mono);background:var(--hover);color:var(--blush);padding:.12em .38em;border-radius:4px} +.c kbd,.c2 kbd{font-size:.72em;vertical-align:.12em} +.c b,.c strong{font-weight:600} +.b-h1{margin-top:24px}.b-h1 .c{font-size:calc(var(--fs) * 1.75);font-weight:700;letter-spacing:-.022em;line-height:1.3}.b-h1{--gt:8px} +.b-h2{margin-top:18px}.b-h2 .c{font-size:calc(var(--fs) * 1.34);font-weight:650;letter-spacing:-.015em;line-height:1.38}.b-h2{--gt:5px} +.b-h3{margin-top:10px}.b-h3 .c{font-size:calc(var(--fs) * 1.09);font-weight:600;line-height:1.5} +.bul{width:22px;height:28px;flex:none;display:grid;place-items:center} +.bul::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--ink-2)} +.morph .bul::before{animation:chipIn .4s var(--spring)} +.check{width:18px;height:18px;margin:5px 2px 0;flex:none;border-radius:5px;box-shadow:inset 0 0 0 1.6px var(--ink-3);display:grid;place-items:center;color:var(--blush-ink);transition:background .2s,box-shadow .2s} +.check:hover{box-shadow:inset 0 0 0 1.6px var(--ink-2)} +.check .i{width:13px;height:13px;stroke-width:2.6;stroke-dasharray:22;stroke-dashoffset:22;transition:stroke-dashoffset .3s var(--ease) .05s} +.check.on{background:var(--blush);box-shadow:inset 0 0 0 1.6px var(--blush)} +.check.on .i{stroke-dashoffset:0} +.check.bump{animation:bump .4s var(--spring)} +.b-todo .c{text-decoration:line-through;text-decoration-color:transparent;text-decoration-thickness:1.5px;transition:color .3s,text-decoration-color .3s} +.b-todo.done .c{color:var(--ink-3);text-decoration-color:var(--ink-3)} +.b-quote .c{border-left:2px solid var(--blush-line);border-radius:0;padding-left:16px;font-size:calc(var(--fs) * 1.06);color:var(--ink-2);margin:4px 0} +.callout{flex:1;min-width:0;display:flex;gap:12px;padding:14px 16px;border-radius:9px;background:var(--blush-soft);margin:4px 0} +.callout-ico{color:var(--blush);padding-top:4px;display:grid;cursor:pointer} +.callout-ico .i{width:18px;height:18px} +.callout[data-kind="tip"] { background: rgba(147, 185, 141, 0.15); } +:root[data-theme="light"] .callout[data-kind="tip"] { background: rgba(95, 143, 88, 0.13); } +.callout[data-kind="tip"] .callout-ico { color: var(--ok); } +.callout[data-kind="warning"] { background: rgba(232, 187, 131, 0.15); } +:root[data-theme="light"] .callout[data-kind="warning"] { background: rgba(164, 104, 29, 0.13); } +.callout[data-kind="warning"] .callout-ico { color: var(--tn); } +.callout[data-kind="caution"] { background: rgba(234, 145, 136, 0.15); } +:root[data-theme="light"] .callout[data-kind="caution"] { background: rgba(180, 73, 62, 0.13); } +.callout[data-kind="caution"] .callout-ico { color: var(--danger); } +.b-callout{--gt:18px} +.hr{flex:1;height:1px;background:var(--line-2);margin:14px 2px;transform-origin:left} +.morph .hr,.enter-b .hr{animation:draw .5s var(--ease)} +.code{flex:1;min-width:0;background:var(--code-bg);border-radius:9px;box-shadow:0 0 0 1px var(--line);margin:6px 0;overflow:hidden} +.b-code{--gt:12px} +.code-bar{display:flex;align-items:center;justify-content:space-between;padding:8px 10px 0 12px} +.code-bar button{font:500 11px var(--f-mono);color:var(--ink-3);padding:3px 7px;border-radius:5px;transition:background .15s,color .15s,opacity .2s} +.code-bar button:hover{background:var(--hover);color:var(--ink)} +.code-bar .copy{opacity:0} +.code:hover .copy{opacity:1} +pre.c{margin:0;padding:6px 18px 16px;font:calc(var(--fs) * .84)/1.7 var(--f-mono);white-space:pre;overflow-x:auto;tab-size:2} +.tk{color:var(--tk)}.ts{color:var(--ts)}.tc{color:var(--tc);font-style:italic}.tn{color:var(--tn)}.tf{color:var(--tf)} +.tg{flex:1;min-width:0} +.tg-head{display:flex;align-items:flex-start;gap:2px} +.tg-chev{width:24px;height:24px;margin-top:2px;display:grid;place-items:center;border-radius:5px;color:var(--ink-2);flex:none;transition:transform .35s var(--spring),background .15s} +.tg-chev:hover{background:var(--hover)} +.tg-chev .i{width:13px;height:13px;stroke-width:2.2} +.tg.open .tg-chev{transform:rotate(90deg)} +.tg-body{display:grid;grid-template-rows:0fr;transition:grid-template-rows .4s var(--ease)} +.tg.open .tg-body{grid-template-rows:1fr} +.tg-in{overflow:hidden;min-height:0;padding-left:26px} +.tg-in .c2{color:var(--ink-2);opacity:0;transform:translateY(-4px);transition:opacity .3s,transform .35s var(--ease);padding-bottom:4px} +.tg.open .tg-in .c2{opacity:1;transform:none} +.num{min-width:22px;height:28px;flex:none;display:flex;align-items:center;justify-content:flex-end;padding-right:4px;color:var(--ink-2);font-variant-numeric:tabular-nums;font-size:15px} +.num::before{content:counter(num) "."} +.b-number{counter-increment:num} +.b-number:not(.b-number + .b-number){counter-reset:num} +.morph .num::before{animation:chipIn .4s var(--spring)} +.c s,.c2 s,.c strike,.c2 strike{text-decoration-thickness:1.5px;color:var(--ink-2)} +.pop.fmt{display:flex;gap:2px;padding:4px;border-radius:7px;z-index:75} +.fmt[hidden]{display:none} +.fmt button{min-width:30px;height:30px;padding:0 7px;display:grid;place-items:center;border-radius:6px;color:var(--ink-2);font-size:14px;transition:background .12s,color .12s} +.fmt button:hover{background:var(--hover);color:var(--ink)} +.fmt button.on{background:var(--blush-soft);color:var(--blush)} +.fmt button b{font-weight:700} +.fmt button i{font-family:Georgia,serif;font-size:15px} +.fmt .i{width:15px;height:15px} +.pop.bmenu{width:224px;max-height:min(520px,calc(100vh - 16px));overflow:auto;transform-origin:top left;z-index:75} +.bmenu .mi{height:32px} +.bmenu .glyph{width:22px;height:22px;display:grid;place-items:center;border-radius:5px;background:var(--hover);font-size:11px;font-weight:700;color:var(--ink-2);flex:none} +.bmenu .glyph .i{width:13px;height:13px} +.bmenu .mi.cur{color:var(--ink)} +.bmenu .mi-on{margin-left:auto;color:var(--blush)} +.bmenu kbd{margin-left:auto} +.block.menu-open>.gutter{opacity:1;transform:none} +.block.menu-open>.gutter .g-drag{background:var(--hover);color:var(--ink)} +::highlight(find){background:color-mix(in oklab,var(--blush) 30%,transparent);color:inherit} +::highlight(find-current){background:var(--blush);color:var(--blush-ink)} +.findbar.pop{position:absolute;top:54px;right:14px;z-index:40;display:flex;align-items:center;gap:4px;padding:5px 5px 5px 12px;border-radius:8px;color:var(--ink-3);animation:pop .22s var(--spring)} +.findbar input{width:200px;height:28px;border:0;outline:0;background:none;color:var(--ink);font:13.5px var(--f-ui)} +.find-n{min-width:64px;text-align:right;font-size:12px;color:var(--ink-3);font-variant-numeric:tabular-nums;padding-right:4px;white-space:nowrap} +.findbar .btn-icon{width:28px;height:28px} +.findbar .up{rotate:-90deg} +.findbar .down{rotate:90deg} +/* kanban board: boards use the full width, with the page head lined up on the same left edge */ +.page.wide{max-width:none;padding:0 var(--side)} +.board{overflow-x:auto;overflow-y:hidden;padding:6px var(--side) 16px;margin-top:10px;animation:rise .5s var(--ease) both .08s} +.kcols{position:relative;display:flex;align-items:flex-start;gap:12px;width:max-content} +.kcol{width:276px;flex:none;display:flex;flex-direction:column;background:var(--island-2);border-radius:10px;box-shadow:0 0 0 1px var(--line);padding:6px 6px 8px;transition:opacity .2s} +.kcol.lifted{opacity:.35} +.khead{display:flex;align-items:center;gap:2px;padding:4px 4px 6px 6px;cursor:grab;min-height:38px} +.kh-title{flex:1;min-width:0;text-align:left;font-weight:650;font-size:13.5px;padding:4px 6px;border-radius:5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:text;transition:background .15s} +.kh-title:hover{background:var(--hover)} +.kh-input{flex:1;min-width:0;height:28px;border:0;outline:0;border-radius:5px;padding:0 6px;background:var(--island);box-shadow:0 0 0 1.5px var(--blush-line);color:var(--ink);font:650 13.5px var(--f-ui)} +.kh-n{font-size:12px;color:var(--ink-3);padding:0 6px;font-variant-numeric:tabular-nums} +.kh-btn{width:26px;height:26px;opacity:0;transition:opacity .15s,background .15s} +.kh-btn .i{width:14px;height:14px} +.kcol:hover .kh-btn,.kh-btn:focus-visible{opacity:1} +.kcards{position:relative;display:flex;flex-direction:column;gap:6px;min-height:8px} +.kcard{position:relative;display:flex;align-items:flex-start;gap:8px;padding:9px 8px 9px 10px;border-radius:7px;background:var(--raise);box-shadow:0 0 0 1px var(--line),0 1px 2px rgba(0,0,0,.08);cursor:grab;transition:box-shadow .15s,opacity .2s,transform .2s var(--ease)} +.kcard:hover{box-shadow:0 0 0 1px var(--line-2),0 4px 14px -6px rgba(0,0,0,.3)} +.kcard.editing{cursor:text;box-shadow:0 0 0 1.5px var(--blush-line)} +.kcard.lifted{opacity:.3} +.kcheck{width:16px;height:16px;margin-top:2px;flex:none;border-radius:4px;box-shadow:inset 0 0 0 1.5px var(--ink-3);display:grid;place-items:center;color:var(--blush-ink);transition:background .2s,box-shadow .2s} +.kcheck .i{width:11px;height:11px;stroke-width:2.8;opacity:0;transition:opacity .2s} +.kcheck.on{background:var(--blush);box-shadow:inset 0 0 0 1.5px var(--blush)} +.kcheck.on .i{opacity:1} +.kc-text{flex:1;min-width:0;font-size:calc(var(--fs) * .84);line-height:1.5;word-wrap:break-word;user-select:text;cursor:inherit} +.kcard.done .kc-text{color:var(--ink-3);text-decoration:line-through;text-decoration-color:var(--ink-3)} +.kc-text code{font:500 .86em var(--f-mono);background:var(--hover);color:var(--blush);padding:.1em .35em;border-radius:4px} +.kc-text b{font-weight:600} +.kedit{flex:1;min-width:0;resize:none;border:0;outline:0;background:none;color:var(--ink);font:calc(var(--fs) * .84)/1.5 var(--f-ui);padding:0;overflow:hidden} +.kmore{position:absolute;top:4px;right:4px;width:24px;height:24px;background:var(--raise);opacity:0;transition:opacity .15s} +.kmore .i{width:13px;height:13px} +.kcard:hover .kmore,.kmore:focus-visible{opacity:1} +.kadd{display:flex;align-items:center;gap:7px;margin-top:6px;padding:7px 8px;border-radius:6px;color:var(--ink-3);font-size:13px;text-align:left;transition:background .15s,color .15s} +.kadd:hover{background:var(--hover);color:var(--ink)} +.kadd .i{width:14px;height:14px} +.kaddcol{width:236px;flex:none;display:flex;align-items:center;gap:8px;height:44px;padding:0 14px;border-radius:9px;color:var(--ink-3);box-shadow:inset 0 0 0 1.5px var(--line-2);font-size:13.5px;transition:background .15s,color .15s} +.kaddcol:hover{background:var(--hover);color:var(--ink)} +.kdrop{position:absolute;left:4px;right:4px;height:3px;border-radius:3px;background:var(--blush);pointer-events:none} +.kdrop-col{position:absolute;top:0;bottom:0;width:3px;border-radius:3px;background:var(--blush);pointer-events:none} +.kghost{position:fixed;left:0;top:0;z-index:90;pointer-events:none;opacity:.95;box-shadow:0 0 0 1px var(--line-2),var(--shadow)!important;overflow:hidden} +.kghost .kmore,.kghost .kh-btn{display:none} +body.board-dragging,body.board-dragging *{cursor:grabbing!important;user-select:none!important} +.pop.kmenu{width:220px;transform-origin:top left;z-index:75} +/* right-click menu for text */ +.pop.tmenu{width:248px;transform-origin:top left;z-index:78;-webkit-app-region:no-drag} +.tmenu .mi{height:32px} +.tmenu .mi.act{background:var(--hover);color:var(--ink)} +.tmenu .mi:disabled{opacity:.45;cursor:default;background:none} +.tmenu .mi.strong{font-weight:600;color:var(--ink)} +.tmenu .mi-pad{width:16px;flex:none} +.tmenu .mi-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.tmenu kbd{margin-left:8px} +.tm-fmt{display:flex;gap:2px;padding:2px} +.tm-fmt button{flex:1;height:30px;display:grid;place-items:center;border-radius:6px;color:var(--ink-2);font-size:14px;transition:background .12s,color .12s} +.tm-fmt button:hover{background:var(--hover);color:var(--ink)} +.tm-fmt button.on{background:var(--blush-soft);color:var(--blush)} +.tm-fmt button i{font-family:Georgia,serif;font-size:15px} +.tm-fmt .i{width:15px;height:15px} +/* ==highlight== */ +mark{background:var(--mark);color:inherit;border-radius:3px;padding:0 .12em} +.fmt-mark{background:var(--mark);padding:0 5px;border-radius:4px;font-weight:600} +/* footnotes: [^id] / ^[inline] references, and "[^id]: text" blocks */ +sup.fn{font-size:.7em;line-height:0;vertical-align:super;padding:0 .08em 0 .1em;margin:0;border-radius:4px;color:var(--blush);font-weight:650;font-variant-numeric:tabular-nums;cursor:pointer;user-select:none;transition:background .15s} +sup.fn:hover{background:var(--blush-soft)} +sup.fn.missing{color:var(--ink-3);text-decoration:underline dotted;text-underline-offset:2px} +.b-footnote .c{font-size:calc(var(--fs) * .88);color:var(--ink-2)} +.fn-num{min-width:22px;height:22px;margin-top:2px;padding:0 6px;flex:none;display:grid;place-items:center;border-radius:6px;font-size:11.5px;font-weight:650;font-variant-numeric:tabular-nums;color:var(--blush);background:var(--blush-soft);transition:filter .15s} +button.fn-num:hover{filter:brightness(1.15)} +.b-footnote.orphan .fn-num{color:var(--ink-3);background:var(--hover)} +.block:not(.b-footnote)+.b-footnote,.eb:not(.b-footnote)+.b-footnote{margin-top:18px;padding-top:12px;border-top:1px solid var(--line);--gt:14px;border-radius:0} +.pop.fn-tip{max-width:340px;padding:9px 12px;border-radius:9px;font-size:13px;line-height:1.55;color:var(--ink-2);z-index:76;pointer-events:none} +.fn-tip[hidden]{display:none} +.fn-tip-n{display:inline-block;margin-right:7px;font-weight:650;color:var(--blush);font-variant-numeric:tabular-nums} +/* embeds: ![[Page]] shows a note inline, read-only */ +.b-embed{--gt:10px} +.emb{flex:1;min-width:0;margin:6px 0;border-radius:10px;background:var(--island-2);box-shadow:inset 3px 0 0 var(--blush-line),0 0 0 1px var(--line);overflow:hidden} +.emb.missing{box-shadow:inset 3px 0 0 var(--line-2),0 0 0 1px var(--line)} +.emb-head{display:flex;align-items:center;padding:8px 10px 0 10px} +.emb-title{display:flex;align-items:center;gap:7px;min-width:0;max-width:100%;padding:3px 7px;border-radius:6px;color:var(--ink-3);font-size:12px;font-weight:600;transition:background .15s,color .15s} +.emb-title:hover{background:var(--hover);color:var(--ink)} +.emb-title .i{width:13px;height:13px;flex:none} +.emb-title span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.emb-body{position:relative;padding:2px 16px 12px 16px} +.emb-body>.eb:first-child{margin-top:0} +.emb:not(.open) .emb-body.long{max-height:320px;overflow:hidden} +.emb:not(.open) .emb-body.long::after{content:"";position:absolute;left:0;right:0;bottom:0;height:72px;background:linear-gradient(transparent,var(--island-2));pointer-events:none} +.emb-more{display:block;width:100%;padding:7px;border-top:1px solid var(--line);color:var(--ink-3);font-size:12px;transition:background .15s,color .15s} +.emb-more:hover{background:var(--hover);color:var(--ink)} +.emb-note{margin:8px 2px 2px;color:var(--ink-3);font-size:13.5px} +.emb-nested{display:flex;align-items:center;gap:7px;color:var(--ink-3);font-size:13.5px} +.emb-nested .i{width:14px;height:14px} +.eb{display:flex;align-items:flex-start;gap:6px;margin:1px 0} +.emb .tbl{padding:0;margin:4px 0} +.ecell{padding:7px 11px;font-size:calc(var(--fs) * .92);line-height:1.55;min-height:1.55em;word-wrap:break-word;text-align:inherit} +.tbl th .ecell{font-weight:600} +.emb .lang{font:500 11px var(--f-mono);color:var(--ink-3);padding:3px 7px} +.etg{flex:1;min-width:0} +.etg>summary{cursor:pointer;font-weight:550} +.etg-body{color:var(--ink-2);padding:2px 0 4px 20px} +/* outline: docked to the right of the page */ +.pagewrap{flex:1;min-height:0;display:flex} +.pagewrap .scroller{min-width:0} +.outline{width:236px;flex:none;min-height:0;display:flex;flex-direction:column;border-left:1px solid var(--line);animation:fade .25s ease} +.ol-head{display:flex;align-items:center;gap:8px;padding:10px 8px 6px 16px;font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3)} +.ol-head span{flex:1} +.ol-head em{font-style:normal;font-weight:500;letter-spacing:0;font-variant-numeric:tabular-nums} +.ol-head .btn-icon{width:26px;height:26px} +.ol-list{flex:1;min-height:0;overflow:auto;padding:2px 8px 24px;display:flex;flex-direction:column;gap:1px} +.ol-item{display:block;width:100%;text-align:left;white-space:normal;padding:5px 8px 5px calc(10px + var(--lv,0) * 14px);border-radius:7px;color:var(--ink-2);font-size:13px;line-height:1.4;transition:background .15s,color .15s;position:relative} +.ol-item span{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word} +.ol-item.top{color:var(--ink);font-weight:550} +.ol-item:hover{background:var(--hover);color:var(--ink)} +.ol-item.on{background:var(--blush-soft);color:var(--ink)} +.ol-item.on::before{content:"";position:absolute;left:0;top:6px;bottom:6px;width:2px;border-radius:2px;background:var(--blush)} +.ol-empty{margin:6px 10px;color:var(--ink-3);font-size:12.5px;line-height:1.7} +.block.flash{animation:flash 1.3s var(--ease)} +@keyframes flash{0%,35%{background:var(--blush-soft)}100%{background:transparent}} +.main:has(.outline) .findbar.pop{right:250px} +.btn-icon.on{color:var(--blush)} +@media (max-width:760px){ + .outline{position:absolute;top:50px;right:0;bottom:0;z-index:20;background:var(--island);box-shadow:-12px 0 30px -12px rgba(0,0,0,.4);border-radius:0 0 12px 0} +} +/* graph view */ +.graph-view{flex:1;min-height:0;display:flex;flex-direction:column} +.graph-back{color:var(--ink-2)} +.graph-back .back{rotate:180deg;width:14px;height:14px} +.graph-title{flex:1;display:flex;align-items:center;gap:8px;font-weight:600;font-size:13px;color:var(--ink);padding-left:4px} +.graph-top .btn-icon.on{color:var(--blush)} +.graph-canvas{position:relative;flex:1;min-height:0;overflow:hidden;border-radius:0 0 12px 12px} +.graph-canvas canvas{display:block;cursor:grab;touch-action:none} +.graph-empty{position:absolute;inset:0;display:grid;place-items:center;color:var(--ink-3);pointer-events:none} +.pop.graph-panel{position:absolute;top:10px;right:10px;width:250px;padding:10px;border-radius:10px;z-index:5;display:flex;flex-direction:column;gap:8px;animation:pop .22s var(--spring)} +.gp-search{display:flex;align-items:center;gap:8px;height:32px;padding:0 10px;border-radius:7px;background:var(--hover);color:var(--ink-3)} +.gp-search input{flex:1;min-width:0;border:0;outline:0;background:none;color:var(--ink);font:13px var(--f-ui)} +.gp-seg{display:flex} +.gp-seg button{flex:1;justify-content:center} +.gp-row{display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12.5px;color:var(--ink-2);min-height:28px} +.gp-row input[type=range]{width:110px;accent-color:var(--blush)} +.gp-row .seg3 button{padding:0 10px} +.qa.on{background:var(--blush-soft);color:var(--ink)} +/* web links */ +a.xl{color:var(--blush);text-decoration:underline;text-decoration-color:var(--blush-line);text-decoration-thickness:1.5px;text-underline-offset:3px;transition:text-decoration-color .15s} +a.xl:hover{text-decoration-color:var(--blush)} +.kc-text a.xl{cursor:pointer} +.pop.lbub{display:flex;align-items:center;gap:2px;padding:4px 4px 4px 10px;border-radius:7px;z-index:76;max-width:min(440px,calc(100vw - 16px))} +.lbub[hidden]{display:none} +.lb-url{display:flex;align-items:center;gap:7px;min-width:0;margin-right:6px;color:var(--ink-2);font-size:12.5px} +.lb-url span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px} +.lb-url .i{width:14px;height:14px;color:var(--ink-3)} +.lbub button{height:28px;min-width:28px;padding:0 8px;display:grid;place-items:center;border-radius:6px;color:var(--ink-2);font-size:12.5px;font-weight:550;transition:background .12s,color .12s} +.lbub button:hover{background:var(--hover);color:var(--ink)} +.lbub button[data-b="open"]{color:var(--blush)} +.lbub button[data-b="unlink"]:hover{color:var(--danger)} +.lbub .i{width:14px;height:14px} +.pop.fmt.url{align-items:center;gap:6px;padding:4px 4px 4px 10px} +.fmt-ico{display:grid;color:var(--ink-3)} +.fmt-url{width:260px;height:30px;border:0;outline:0;background:none;color:var(--ink);font:13.5px var(--f-ui)} +.fmt-url.bad{color:var(--danger);animation:shake .3s} +@keyframes shake{25%{transform:translateX(-3px)}75%{transform:translateX(3px)}} +/* tables */ +.tbl{position:relative;flex:1;min-width:0;margin:8px 0;padding:0 18px 18px 0} +.tbl-scroll{overflow-x:auto;border-radius:7px;box-shadow:0 0 0 1px var(--line-2)} +.tbl table{border-collapse:collapse;min-width:100%} +.tbl th,.tbl td{border:1px solid var(--line-2);padding:0;vertical-align:top;min-width:110px;text-align:left} +.tbl tr:first-child th{border-top:0} +.tbl tr:last-child td{border-bottom:0} +.tbl th:first-child,.tbl td:first-child{border-left:0} +.tbl th:last-child,.tbl td:last-child{border-right:0} +.tbl th{background:var(--island-2)} +.tcell{outline:none;padding:7px 11px;font-size:calc(var(--fs) * .92);line-height:1.55;min-height:1.55em;white-space:pre-wrap;word-wrap:break-word;caret-color:var(--blush);text-align:inherit} +.tbl th .tcell{font-weight:600} +.tcell:focus{box-shadow:inset 0 0 0 1.5px var(--blush-line);border-radius:2px} +.tcell:empty::before{content:attr(data-ph);color:var(--ink-3);pointer-events:none} +.tcell code{font:500 .86em var(--f-mono);background:var(--hover);color:var(--blush);padding:.1em .35em;border-radius:4px} +.tbl-add{position:absolute;display:grid;place-items:center;border-radius:5px;color:var(--ink-3);background:var(--hover);opacity:0;transition:opacity .15s,background .15s,color .15s} +.tbl-add .i{width:13px;height:13px} +.tbl-add:hover{background:var(--press);color:var(--ink)} +.tbl-add-col{top:0;right:0;bottom:18px;width:14px} +.tbl-add-row{left:0;right:18px;bottom:0;height:14px} +.block:hover .tbl-add{opacity:1} +.b-table{--gt:10px} +.wl{color:var(--blush);font-weight:500;text-decoration:underline;text-decoration-color:var(--blush-line);text-decoration-thickness:1.5px;text-underline-offset:3px;cursor:pointer;border-radius:4px;padding:0 1px;transition:background .15s,text-decoration-color .15s} +.wl:hover{background:var(--blush-soft);text-decoration-color:var(--blush)} +.wl.missing{color:var(--ink-3);text-decoration-style:dashed;text-decoration-color:var(--line-2)} +.img{flex:1;min-width:0;margin:8px 0;position:relative;border-radius:9px} +.img img{display:block;max-width:100%;max-height:70vh;border-radius:9px;box-shadow:0 0 0 1px var(--line)} +.img-x{position:absolute;top:8px;right:8px;width:28px;height:28px;display:grid;place-items:center;border-radius:6px;background:var(--raise);color:var(--ink-2);box-shadow:0 0 0 1px var(--line-2);opacity:0;transition:opacity .15s,color .15s} +.img:hover .img-x{opacity:1} +.img-x:hover{color:var(--danger)} +.img-miss{display:none;align-items:center;gap:8px;padding:14px 16px;border-radius:9px;background:var(--hover);color:var(--ink-3);font-size:13px;font-family:var(--f-mono);word-break:break-all} +.img.broken img{display:none} +.img.broken .img-miss{display:flex} +.img.broken .img-x{opacity:1;top:50%;translate:0 -50%} +.b-image{--gt:12px} +.backlinks{margin:8px 0 48px;padding-top:16px;border-top:1px solid var(--line);display:flex;flex-direction:column;gap:2px} +.backlinks h2{display:flex;align-items:center;gap:8px;margin:0 0 6px;font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3)} +.bl{display:flex;align-items:flex-start;gap:10px;padding:8px 10px;border-radius:7px;text-align:left;color:var(--ink-2);transition:background .15s,color .15s} +.bl:hover{background:var(--hover);color:var(--ink)} +.bl .row-ico{padding-top:2px} +.bl-t{display:flex;flex-direction:column;min-width:0} +.bl-t b{font-weight:600} +.bl-t small{color:var(--ink-3);font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.page-ghost{position:fixed;left:0;top:0;z-index:90;pointer-events:none;padding:5px 11px;border-radius:6px;background:var(--raise);color:var(--ink);font-size:13px;box-shadow:0 0 0 1px var(--line-2),var(--shadow);white-space:nowrap} +body.page-dragging,body.page-dragging *{cursor:grabbing!important;user-select:none!important} +.row.lifted{opacity:.4} +.favs{display:flex;flex-direction:column;margin:0 -4px;padding:0 4px} +.row.fav{padding-left:10px} +.foot-btn{width:28px;height:28px} +.foot-btn .i{width:15px;height:15px;transition:transform .5s var(--spring)} +.foot-trash{margin-left:auto} +.foot-btn[aria-label="Settings"]:hover .i{transform:rotate(60deg)} +/* settings */ +.panel.settings{width:min(600px,100%);max-height:calc(100vh - 12vh - 32px);display:flex;flex-direction:column} +.set-top{display:flex;align-items:center;gap:10px;padding:14px 14px 10px 18px;border-bottom:1px solid var(--line)} +.set-top h2{margin:0;font-size:15px;font-weight:650;flex:1} +.set-body{overflow:auto;padding:4px 18px 18px} +.set-body h3{margin:18px 0 6px;font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3)} +.set-row{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:9px 0;border-bottom:1px solid var(--line)} +.set-t{display:flex;flex-direction:column;min-width:0;gap:2px} +.set-t b{font-weight:550} +.set-t small{color:var(--ink-3);font-size:12px;line-height:1.4} +.set-t small.mono{font-family:var(--f-mono);font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:300px} +.set-btns{display:flex;gap:6px;flex:none} +.set-btns .btn{height:30px;padding:0 12px} +.seg3{display:flex;gap:2px;padding:3px;border-radius:7px;background:var(--hover);flex:none} +.seg3 button{display:flex;align-items:center;gap:6px;height:28px;padding:0 11px;border-radius:5px;font-size:12.5px;font-weight:550;color:var(--ink-3);transition:background .15s,color .15s} +.seg3 button .i{width:14px;height:14px} +.seg3 button.on{background:var(--raise);color:var(--ink);box-shadow:0 1px 3px rgba(0,0,0,.18)} +.seg3 button:not(.on):hover{color:var(--ink)} +.set-size{display:flex;align-items:center;gap:9px;flex:none} +.set-size .a-small{font-size:11px;color:var(--ink-3)} +.set-size .a-big{font-size:17px;color:var(--ink-3)} +.set-size input[type=range]{width:150px;accent-color:var(--blush)} +.set-val{min-width:46px;height:26px;padding:0 6px;border-radius:5px;font:500 12px var(--f-mono);color:var(--ink-2);background:var(--hover)} +.set-val:disabled{opacity:1;cursor:default} +.set-val:not(:disabled):hover{color:var(--blush)} +.set-preview{margin:2px 0 4px;padding:10px 14px;border-radius:7px;background:var(--island-2);line-height:1.6;color:var(--ink-2);user-select:none} +.set-note{margin:8px 0 0;color:var(--ink-3);font-size:12px} +.settings select{min-width:200px;max-width:240px;height:32px;padding:0 10px;border-radius:6px;border:0;background:var(--hover);color:var(--ink);font:13px var(--f-ui);box-shadow:inset 0 0 0 1px var(--line);cursor:pointer} +.settings select:focus-visible{outline:2px solid var(--blush-line)} +.tgl{position:relative;width:40px;height:23px;border-radius:99px;background:var(--hover);box-shadow:inset 0 0 0 1px var(--line-2);flex:none;transition:background .2s} +.tgl span{position:absolute;top:3px;left:3px;width:17px;height:17px;border-radius:50%;background:var(--ink-3);transition:transform .3s var(--spring),background .2s} +.tgl.on{background:var(--blush)} +.tgl.on span{transform:translateX(17px);background:var(--blush-ink)} +.star-btn.on{color:var(--blush)} +.star-btn.on .i{fill:currentColor} +.trash-list{flex:1;overflow:auto;padding:6px;min-height:120px;max-height:min(460px,60vh)} +.tr{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:7px;transition:background .15s} +.tr:hover{background:var(--hover)} +.tr-t{flex:1;min-width:0;display:flex;flex-direction:column} +.tr-t b{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.tr-t small{color:var(--ink-3);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.tr .btn{height:30px;padding:0 11px;font-size:12.5px} +.tr .btn.danger.armed,.trash-foot .btn.danger.armed{color:var(--danger);background:color-mix(in oklab,var(--danger) 14%,transparent)} +.trash-top{display:flex;align-items:center;gap:10px;padding:14px 14px 10px 18px;border-bottom:1px solid var(--line)} +.trash-top h2{margin:0;font-size:15px;font-weight:650;flex:1} +.trash-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 14px;border-top:1px solid var(--line);color:var(--ink-3);font-size:12px} +.trash-empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:40px 20px;color:var(--ink-3)} +.row.drop-in{background:var(--blush-soft);box-shadow:inset 0 0 0 1.5px var(--blush-line);color:var(--ink)} +.tree.drop-root,.sec.drop-root{background:var(--blush-soft);border-radius:7px} +.drop{position:absolute;left:0;right:0;top:0;height:3px;border-radius:3px;background:var(--blush);opacity:0;pointer-events:none;transition:top .12s var(--ease),opacity .12s} +.drop.show{opacity:1} +.tail{height:32vh;cursor:text} + +/* floating things */ +.pop{position:fixed;z-index:70;background:var(--raise);border-radius:9px;box-shadow:0 0 0 1px var(--line-2),var(--shadow);padding:6px} +.pop:not([hidden]){animation:pop .22s var(--spring)} +.pop-label{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);padding:6px 8px 4px} +.slash{width:300px;max-height:340px;overflow:auto;transform-origin:top left} +.si{display:flex;align-items:center;gap:11px;width:100%;padding:6px 8px;border-radius:6px;text-align:left;transition:background .12s} +.si.on{background:var(--hover)} +.glyph{width:38px;height:38px;flex:none;border-radius:7px;background:var(--island);box-shadow:0 0 0 1px var(--line-2);display:grid;place-items:center;font-weight:600;font-size:13px;color:var(--ink-2);transition:transform .25s var(--spring),color .2s} +.si.on .glyph{transform:scale(1.06);color:var(--blush)} +.glyph .i{width:17px;height:17px} +.si-t{display:flex;flex-direction:column;min-width:0;flex:1;line-height:1.3} +.si-t b{font-weight:500;font-size:13.5px} +.si-t small{color:var(--ink-3);font-size:12px} +.menu{width:236px;transform-origin:top right} +.mi{display:flex;align-items:center;gap:10px;width:100%;height:34px;padding:0 10px;border-radius:6px;color:var(--ink-2);text-align:left;transition:background .12s,color .12s} +.mi:hover{background:var(--hover);color:var(--ink)} +.mi.danger:hover,.mi.armed{color:var(--danger);background:color-mix(in oklab,var(--danger) 12%,transparent)} +.sepline{height:1px;background:var(--line);margin:5px 6px} +.overlay{position:fixed;inset:0;z-index:80;background:var(--scrim);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);display:flex;justify-content:center;align-items:flex-start;padding:12vh 16px 16px} +.overlay:not([hidden]){animation:fade .22s ease} +.overlay:not([hidden]) .panel{animation:panelIn .38s var(--spring)} +.overlay.closing{opacity:0;transition:opacity .16s} +.overlay.closing .panel{transform:scale(.97) translateY(6px);transition:transform .16s} +.panel{width:min(620px,100%);background:var(--raise);border-radius:12px;box-shadow:0 0 0 1px var(--line-2),var(--shadow);overflow:hidden} +.pal-top{display:flex;align-items:center;gap:12px;padding:16px 18px;border-bottom:1px solid var(--line);color:var(--ink-3)} +.pal-top .i{width:19px;height:19px} +.pal-top input{flex:1;min-width:0;border:0;outline:0;background:none;color:var(--ink);font:16.5px var(--f-ui)} +.pal-top input::placeholder{color:var(--ink-3)} +.pal-list{max-height:min(52vh,430px);overflow:auto;padding:6px} +.pi{display:flex;align-items:flex-start;gap:12px;width:100%;padding:9px 10px;border-radius:7px;text-align:left;transition:background .12s} +.pi.on{background:var(--hover)} +.pi .row-ico{padding-top:2px} +.pi.on .row-ico{color:var(--blush)} +.pi-t{display:flex;flex-direction:column;flex:1;min-width:0;gap:1px} +.pi-t b{font-weight:500;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.pi-t small{color:var(--ink-3);font-size:12.5px;line-height:1.45;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} +.pi-path{font:11px var(--f-mono);color:var(--ink-3);padding-top:3px;white-space:nowrap} +.pi mark{background:var(--blush-soft);color:var(--ink);border-radius:3px;padding:0 1px} +.pi-enter{opacity:0;color:var(--ink-3);padding-top:2px;transition:opacity .15s} +.pi.on .pi-enter{opacity:1} +.pal-empty{padding:28px 12px;text-align:center;color:var(--ink-3)} +.pal-foot{display:flex;gap:18px;padding:10px 18px;border-top:1px solid var(--line);color:var(--ink-3);font-size:12px} +.pal-foot span{display:flex;align-items:center;gap:5px} +.exp-top{display:flex;align-items:center;gap:12px;padding:12px 12px 12px 14px;border-bottom:1px solid var(--line)} +.seg{position:relative;display:flex;background:var(--island-2);border-radius:7px;padding:3px;flex:none} +.seg button{position:relative;z-index:1;height:28px;padding:0 14px;border-radius:6px;font-size:13px;color:var(--ink-2);transition:color .25s;min-width:96px} +.seg button.on{color:var(--ink)} +.seg-pill{position:absolute;top:3px;left:3px;height:28px;width:96px;border-radius:6px;background:var(--raise);box-shadow:0 0 0 1px var(--line-2),0 2px 6px rgba(0,0,0,.12);transition:transform .4s var(--spring)} +.seg.md .seg-pill{transform:translateX(0)} +.seg.html .seg-pill{transform:translateX(96px)} +.seg.pdf .seg-pill{transform:translateX(192px)} +.exp-name{flex:1;min-width:0;font:12px var(--f-mono);color:var(--ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.exp-out{margin:0;padding:16px 18px;max-height:min(50vh,420px);overflow:auto;font:12.5px/1.7 var(--f-mono);color:var(--ink-2);white-space:pre-wrap;word-break:break-word;background:var(--code-bg)} +.exp-out.swap{animation:fade .3s ease} +.exp-foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px 12px 18px;border-top:1px solid var(--line);color:var(--ink-3);font-size:12.5px} +.btn{display:inline-flex;align-items:center;gap:7px;height:32px;padding:0 14px;border-radius:6px;font-weight:500;font-size:13px;transition:transform .2s var(--spring),filter .15s;flex:none} +.btn.primary{background:var(--blush);color:var(--blush-ink)} +.btn.primary:hover{filter:brightness(1.06)} +.btn:active{transform:scale(.95)} +.btn .i{width:14px;height:14px} +.toast{position:fixed;left:50%;bottom:24px;z-index:90;display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;background:var(--ink);color:var(--island);font-size:13px;font-weight:500;box-shadow:var(--shadow);opacity:0;transform:translate(-50%,16px) scale(.96);pointer-events:none;transition:opacity .2s,transform .4s var(--spring)} +.toast.show{opacity:1;transform:translate(-50%,0)} +.toast .i{width:15px;height:15px;color:var(--blush)} +.toast.act.show{pointer-events:auto;padding-right:6px} +.toast-btn{margin-left:6px;padding:5px 10px;border-radius:6px;color:var(--blush);font-weight:600;transition:background .15s} +.toast-btn:hover{background:color-mix(in oklab,var(--island) 14%,transparent)} +.scrim{display:none} + +@keyframes rise{from{opacity:0;transform:translateY(9px)}to{opacity:1;transform:none}} +@keyframes blockIn{from{opacity:0;transform:translateY(-6px)}} +@keyframes morph{from{opacity:.35;transform:scale(.98)}} +@keyframes fresh{0%{opacity:0;transform:translateX(-10px)}40%{opacity:1;transform:none;background:var(--blush-soft)}100%{background:transparent}} +@keyframes pop{from{opacity:0;transform:translateY(-4px) scale(.96)}} +@keyframes fade{from{opacity:0}} +@keyframes panelIn{from{opacity:0;transform:translateY(14px) scale(.96)}} +@keyframes pulse{50%{opacity:.35;transform:scale(.6)}} +@keyframes bump{35%{transform:scale(.8)}70%{transform:scale(1.12)}} +@keyframes chipIn{from{opacity:0;transform:scale(.6)}} +@keyframes draw{from{transform:scaleX(0)}} +@keyframes spin{from{transform:rotate(-180deg) scale(.6)}} +@keyframes coverIn{from{opacity:0;transform:scale(1.03)}} +@keyframes drift{from{transform:translate(-4%,-3%) scale(1.1)}to{transform:translate(4%,3%) scale(1.15)}} +::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal} + +@media (max-width:760px){ + .app,.app.collapsed{grid-template-columns:minmax(0,1fr);gap:0;padding:8px} + .sidebar{position:fixed;top:8px;bottom:8px;left:8px;z-index:60;width:min(284px,calc(100% - 56px));transform:translateX(calc(-100% - 20px));box-shadow:0 0 0 1px var(--line-2),var(--shadow)} + .app.collapsed .sidebar{opacity:1;pointer-events:auto} + .app.drawer .sidebar{transform:none} + .sb-inner{width:100%} + .app.drawer .scrim{display:block;position:fixed;inset:0;z-index:55;background:var(--scrim);animation:fade .25s} + .menu-btn,.app.collapsed .menu-btn{display:grid} + .gutter{display:none} + .wc{display:none} + .page{padding:0 18px} + .prop{grid-template-columns:96px minmax(0,1fr)} + .cover{margin:0 6px} +} +@media (max-width:460px){.status span:last-child{display:none}} +@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}} + +/* ---------- desktop shell ---------- */ +.topbar,.ws{-webkit-app-region:drag} +.topbar button,.topbar .menu,.ws button{-webkit-app-region:no-drag} +.ctx{transform-origin:top left;-webkit-app-region:no-drag} +.row-rename{font:inherit;color:var(--ink);background:var(--island);border:0;border-radius:5px;padding:2px 6px;margin:0 -6px;outline:none;box-shadow:0 0 0 1px var(--blush-line)} +.dragstrip{position:fixed;top:0;left:0;right:0;height:10px;-webkit-app-region:drag} +.topbar{position:relative} +.topbar .menu{position:absolute;top:46px;right:10px;transform-origin:top right} +.app.mac .ws{padding-top:34px} +.app.mac.collapsed .topbar{padding-left:84px} +.btn-icon:disabled{opacity:.35;pointer-events:none} +.ws-path{font:11px var(--f-mono);color:var(--ink-3);text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;padding:0;transition:color .15s} +.ws-path:hover{color:var(--ink-2)} +.muted,.empty-tree{color:var(--ink-3);font-size:12.5px;padding:2px 9px;margin:0} +.row.ghost .name{color:var(--ink-3)} +.pv-link{text-align:left;padding:0;border-radius:4px;transition:color .15s} +.pv-link:hover{color:var(--ink)} +.blocks{padding-top:4px} +.empty-page{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;height:70%;color:var(--ink-3)} +.btn{background:var(--hover);color:var(--ink)} +.btn:hover{background:var(--press)} +.btn.primary:hover{background:var(--blush)} +.exp-foot{justify-content:flex-end} +.welcome{height:100%;display:grid;place-items:center;padding:24px;-webkit-app-region:drag} +.welcome-card{width:min(460px,100%);padding:36px 32px 30px;display:flex;flex-direction:column;align-items:flex-start;gap:14px;animation:panelIn .6s var(--spring);-webkit-app-region:no-drag} +.welcome-card h1{margin:6px 0 0;font-size:24px;font-weight:700;letter-spacing:-.02em;text-wrap:balance} +.welcome-card p{margin:0;color:var(--ink-2);font-size:14.5px;line-height:1.6} +.welcome-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px} +.winctl{display:flex;align-items:center;gap:2px;margin-left:6px;padding-left:8px;border-left:1px solid var(--line);-webkit-app-region:no-drag} +.winctl .close:hover{background:var(--danger);color:var(--blush-ink)} +.welcome .winctl{position:fixed;top:10px;right:10px;border-left:0} +.welcome-logo{width:72px;height:72px;margin:-8px 0 -4px -8px} +.ws-mark.big{width:44px;height:44px;border-radius:9px;font-size:19px} + +/* Math and Mermaid */ +.math, .mermaid-wrap { + position: relative; + margin: 12px 0; + border-radius: 12px; + background: var(--ink-bg-2); + border: 1px solid var(--ink-border); + overflow: hidden; +} +.math-preview, .mermaid-preview { + padding: 16px; + background: var(--ink-bg); + border-bottom: 1px solid var(--ink-border); + text-align: center; + overflow-x: auto; +} +.mermaid-preview svg { + max-width: 100%; + height: auto; +} +.math .code-bar, .mermaid-wrap .code-bar { + position: absolute; + top: 100%; + right: 0; + transform: translateY(-100%); + display: flex; + opacity: 0.5; +} +.math:focus-within .code-bar, .mermaid-wrap:focus-within .code-bar { + opacity: 1; +} +.math .code-c, .mermaid-wrap .code-c { + padding: 12px 16px; + background: var(--ink-bg-2); + font: 0.85em var(--font-mono); + outline: none; + white-space: pre-wrap; + word-break: break-all; +} + +/* custom properties */ +.custom-pk, .custom-pv { + background: transparent; + border: 1px solid transparent; + color: inherit; + font: inherit; + outline: none; + padding: 0 4px; + border-radius: 4px; + height: 24px; + width: 100%; +} +.custom-pk { + width: 80px; +} +.custom-pk:focus, .custom-pk:hover, .custom-pv:focus, .custom-pv:hover { + background: var(--hover); +} +.prop:hover .chip-x { opacity: 1; } +.prop .chip-x { opacity: 0; pointer-events: none; } +.prop:hover .chip-x { opacity: 0.6; pointer-events: auto; } +.prop:hover .chip-x:hover { opacity: 1; } diff --git a/src/lib/components/PageView.svelte b/src/lib/components/PageView.svelte new file mode 100644 index 0000000..e7fa6b2 --- /dev/null +++ b/src/lib/components/PageView.svelte @@ -0,0 +1,279 @@ + + +
+
+ {#if doc} + + {#each store.tagCounts as [tag]} + + {/each} + + {#key doc.version} +
+ +
+
+
+ +

store.commitTitle(titleEl.textContent)} + onpaste={onTitlePaste} + >

+
+
+ Tags +
+ {#each tags as t (t)} + + {t} + + {/each} + {#if tagInput !== null} + { + if (e.key === 'Enter') { e.preventDefault(); addTag(true); } + if (e.key === 'Escape') { e.preventDefault(); addTag(false); } + }} + onblur={() => addTag(true)} + /> + {:else} + + {/if} +
+
+
+ Created + {fmtDate(created)} +
+
+ File + +
+ {#each Object.keys(doc.meta).filter(k => doc.meta[k] !== undefined && !['icon', 'cover', 'tags', 'created'].includes(k)) as pk (pk)} +
+ + + renameMeta(pk, e.target.value)} /> + +
+ updateMeta(pk, e.target.value)} + placeholder="Empty" + /> + +
+
+ {/each} + {#if addingProp} +
+ { if (e.key === 'Enter') commitNewProp(e.target.value); if (e.key === 'Escape') addingProp = false; }} onblur={(e) => commitNewProp(e.target.value)} /> + +
+ {/if} +
+ +
+
+
+
+ {#if !store.isBoard} + + + {/if} +
+ {#if pickingCover} + { pickingCover = false; coverBtnEl?.focus(); }} + /> + {/if} + {#if picking} + setIcon('')} + onclose={() => { picking = false; iconEl?.focus(); }} + /> + {/if} + {#if store.isBoard} + +
+ {/if} + {/key} + {:else} +
+

This folder has no pages yet.

+ +
+ {/if} +
+{#if store.outlineOpen && doc && !store.isBoard}{/if} +
diff --git a/src/lib/icons.js b/src/lib/icons.js new file mode 100644 index 0000000..fc85fd0 --- /dev/null +++ b/src/lib/icons.js @@ -0,0 +1,148 @@ +// Stroke icons on a 24px grid. Multiple paths are separated by "|". +export const PATHS = { + plus: 'M12 5v14|M5 12h14', + chev: 'M9 6l6 6-6 6', + search: 'M4 11a7 7 0 1 0 14 0a7 7 0 1 0-14 0|M16.5 16.5L21 21', + panel: 'M4 5h16v14H4z|M9.5 5v14', + dots: 'M6 12h.01|M12 12h.01|M18 12h.01', + grip: 'M9 6h.01|M15 6h.01|M9 12h.01|M15 12h.01|M9 18h.01|M15 18h.01', + x: 'M6 6l12 12|M18 6L6 18', + minimize: 'M6 12h12', + maximize: 'M6 6h12v12H6z', + restore: 'M8 9h10v10H8z|M6 14V5h11', + board: 'M4 4h4.5v16H4z|M9.75 4h4.5v11h-4.5z|M15.5 4H20v7h-4.5z', + star: 'M12 3.8l2.5 5.2 5.7.8-4.1 4 1 5.6L12 16.7l-5.1 2.7 1-5.6-4.1-4 5.7-.8z', + undo: 'M9 14L4 9l5-5|M4 9h10.5a5.5 5.5 0 0 1 0 11H11', + hash: 'M5 9h14|M5 15h14|M10 4L8 20|M16 4l-2 16', + list: 'M4 6h16|M4 12h16|M4 18h16', + clock: 'M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0|M12 7.5V12l3 2', + file: 'M7 3h7l5 5v13H7z|M14 3v5h5', + folder: 'M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z', + export: 'M12 15V3|M7 8l5-5 5 5|M5 14v6h14v-6', + copy: 'M9 9h11v11H9z|M5 15V4h11', + trash: 'M4 7h16|M10 11v6|M14 11v6|M6 7l1 13h10l1-13|M9 7V4h6v3', + tick: 'M5 12.5l4.5 4.5L19 7.5', + check: 'M5 12.5l4.5 4.5L19 7.5', + image: 'M4 5h16v14H4z|M4 16l5-5 4 4 3-3 4 4|M15 9h.01', + sun: 'M12 8a4 4 0 1 0 0 8a4 4 0 1 0 0-8|M12 2v2|M12 20v2|M4.9 4.9l1.4 1.4|M17.7 17.7l1.4 1.4|M2 12h2|M20 12h2|M4.9 19.1l1.4-1.4|M17.7 6.3l1.4-1.4', + moon: 'M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5z', + enter: 'M9 10l-5 5 5 5|M20 4v7a4 4 0 0 1-4 4H4', + save: 'M5 4h11l3 3v13H5z|M8 4v5h7V4|M8 20v-6h8v6', + sparkle: 'M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8z|M19 16l.7 1.8 1.8.7-1.8.7L19 21l-.7-1.8-1.8-.7 1.8-.7z', + pen: 'M4 20l4-1 11-11-3-3L5 16z|M14 6l3 3', + leaf: 'M5 19c0-8 5-13 14-14-1 9-6 14-14 14z|M5 19l7-7', + book: 'M5 4h11a3 3 0 0 1 3 3v13H8a3 3 0 0 1-3-3z|M5 17a3 3 0 0 1 3-3h11', + code: 'M8 8l-4 4 4 4|M16 8l4 4-4 4|M13.5 5l-3 14', + compass: 'M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0|M15.5 8.5l-2 5-5 2 2-5z', + cup: 'M5 9h12v5a5 5 0 0 1-5 5h-2a5 5 0 0 1-5-5z|M17 10h1.5a2.5 2.5 0 0 1 0 5H17|M9 3.5v2.5|M13 3.5v2.5', + outline: 'M4 6h16|M8 12h12|M12 18h8', + graph: 'M4 7a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0-5 0|M15 5a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0-5 0|M11 17.5a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0-5 0|M8.9 6.6l6.2-1.2|M7.6 9.1l4.4 6.3|M16.9 7.4l-2.3 7.8', + cut: 'M3 6a3 3 0 1 0 6 0a3 3 0 1 0-6 0|M3 18a3 3 0 1 0 6 0a3 3 0 1 0-6 0|M20 4L8.1 15.9|M14.5 14.5L20 20|M8.1 8.1L12 12', + paste: 'M9 3h6v3H9z|M9 4.5H6V21h12V4.5h-3', + selectAll: 'M4 8V4h4|M16 4h4v4|M20 16v4h-4|M8 20H4v-4|M10.5 4h3|M10.5 20h3|M4 10.5v3|M20 10.5v3', + table: 'M4 5h16v14H4z|M4 10h16|M4 14.5h16|M10 5v14', + link: 'M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1|M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1', + unlink: 'M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1|M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1|M4 4l16 16', + alignLeft: 'M4 6h16|M4 10h10|M4 14h16|M4 18h10', + alignCenter: 'M4 6h16|M7 10h10|M4 14h16|M7 18h10', + alignRight: 'M4 6h16|M10 10h10|M4 14h16|M10 18h10', + minus: 'M5 12h14', + monitor: 'M3 5h18v12H3z|M8 21h8|M12 17v4', + gear: 'M9 12a3 3 0 1 0 6 0a3 3 0 1 0-6 0|M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z', + shuffle: 'M3 7h3c4 0 6 10 10 10h5|M18 14l3 3-3 3|M3 17h3c1.5 0 2.7-1.4 3.8-3.2|M14.2 10.2C15.3 8.4 16.5 7 18 7h3|M18 4l3 3-3 3', + // page icons offered in the picker + home: 'M4 11l8-7 8 7|M6 9.5V20h12V9.5|M10 20v-6h4v6', + heart: 'M12 20s-7.5-4.6-7.5-10.2A4.1 4.1 0 0 1 12 7.4a4.1 4.1 0 0 1 7.5 2.4C19.5 15.4 12 20 12 20z', + bolt: 'M13 3L5 14h6l-1 7 8-11h-6z', + flag: 'M5 21V4|M5 4h12l-2.5 4L17 12H5', + bell: 'M6 16v-5a6 6 0 0 1 12 0v5l1.5 2h-15z|M10 20.5a2 2 0 0 0 4 0', + bookmark: 'M6 3h12v18l-6-4-6 4z', + calendar: 'M4 6h16v14H4z|M4 10h16|M8 3v5|M16 3v5', + camera: 'M4 8h4l2-3h4l2 3h4v11H4z|M9 13.5a3 3 0 1 0 6 0a3 3 0 1 0-6 0', + music: 'M9 17V5l10-2v12|M4 17a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0-5 0|M14 15a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0-5 0', + globe: 'M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0|M3 12h18|M12 3a14 14 0 0 1 0 18|M12 3a14 14 0 0 0 0 18', + map: 'M9 4L3 6v14l6-2 6 2 6-2V4l-6 2z|M9 4v14|M15 6v14', + target: 'M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0|M7 12a5 5 0 1 0 10 0a5 5 0 1 0-10 0|M11 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0', + bulb: 'M9 18h6|M10 21h4|M12 3a6 6 0 0 0-4 10.5c.7.7 1 1.5 1 2.5h6c0-1 .3-1.8 1-2.5A6 6 0 0 0 12 3z', + rocket: 'M12 3c3 2 5 6 4 11l-4 3-4-3c-1-5 1-9 4-11z|M12 9.5h.01|M8 14l-3 3 1 3 3-2|M16 14l3 3-1 3-3-2', + terminal: 'M4 5h16v14H4z|M8 10l3 2-3 2|M13 15h3', + database: 'M5 6c0-1.7 3.1-3 7-3s7 1.3 7 3-3.1 3-7 3-7-1.3-7-3z|M5 6v12c0 1.7 3.1 3 7 3s7-1.3 7-3V6|M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3', + cpu: 'M7 7h10v10H7z|M10 10h4v4h-4z|M9 3v4|M15 3v4|M9 17v4|M15 17v4|M3 9h4|M3 15h4|M17 9h4|M17 15h4', + gamepad: 'M6.5 8h11a4 4 0 0 1 3.9 4.9l-.9 3.6a2.4 2.4 0 0 1-4.2 1L15 16H9l-1.3 1.5a2.4 2.4 0 0 1-4.2-1l-.9-3.6A4 4 0 0 1 6.5 8z|M8 11v3|M6.5 12.5h3|M15 11.5h.01|M17 13.5h.01', + palette: 'M12 3a9 9 0 0 0 0 18c1.1 0 2-.9 2-2 0-.5-.2-1-.5-1.3-.3-.4-.5-.8-.5-1.2 0-1.1.9-2 2-2h2.5A4.5 4.5 0 0 0 21 10c0-3.9-4-7-9-7z|M7.5 11h.01|M10 7.5h.01|M14.5 7.5h.01', + chart: 'M4 4v16h16|M8 16v-4|M12 16V8|M16 16v-6', + wallet: 'M4 7h15a1 1 0 0 1 1 1v11H5a1 1 0 0 1-1-1z|M4 7l11-3v3|M16 13h.01', + briefcase: 'M4 8h16v11H4z|M9 8V5h6v3|M4 13h16', + school: 'M2 9l10-5 10 5-10 5z|M6 11v5c0 1.5 2.7 3 6 3s6-1.5 6-3v-5|M22 9v6', + users: 'M5.5 7.5a3.5 3.5 0 1 0 7 0a3.5 3.5 0 1 0-7 0|M3 20c0-3.3 2.7-6 6-6s6 2.7 6 6|M16 4.5a3.5 3.5 0 0 1 0 6.5|M18 14c1.8.9 3 2.8 3 5', + user: 'M8 8a4 4 0 1 0 8 0a4 4 0 1 0-8 0|M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8', + mail: 'M3 6h18v12H3z|M3 7l9 6 9-6', + chat: 'M4 5h16v11H9l-5 4z', + lock: 'M6 11h12v9H6z|M8.5 11V8a3.5 3.5 0 0 1 7 0v3', + key: 'M4 11a4 4 0 1 0 8 0a4 4 0 1 0-8 0|M12 11h9|M18 11v3|M15 11v2', + tree: 'M12 3l6 8h-3l4 6H5l4-6H6z|M12 17v4', + send: 'M21 3L3 10.5l7 2.5 2.5 7z|M10 13L21 3', + pulse: 'M3 12h4l3-7 4 14 3-7h4', + food: 'M7 3v8|M5 3v5a2 2 0 0 0 4 0V3|M7 11v10|M17 3c-2 1.5-3 4-3 7h3v11', + cart: 'M3 4h2.5l2.2 11h10.6L20.5 7H7|M9 19.5h.01|M17 19.5h.01', + gift: 'M4 9h16v4H4z|M5 13h14v8H5z|M12 9v12|M12 9C10.5 5 6 5 6 7.5S10 9 12 9|M12 9c1.5-4 6-4 6-1.5S14 9 12 9', + tag: 'M3 12V4h8l10 10-8 8z|M7.5 8h.01', + inbox: 'M3 13l3-8h12l3 8v6H3z|M3 13h5l1 2h6l1-2h5', + layers: 'M12 3l9 5-9 5-9-5z|M3 13l9 5 9-5', + check2: 'M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0|M8 12.5l2.5 2.5L16 9.5' +}; + +// The stroke icons offered in the icon picker, with words to find them by. +export const PICKER_ICONS = [ + ['sparkle', 'star new magic'], ['pen', 'write edit note'], ['leaf', 'nature plant green'], ['book', 'read study learn'], + ['code', 'dev programming'], ['moon', 'night dark sleep'], ['compass', 'explore direction travel'], ['cup', 'coffee tea break'], + ['home', 'house'], ['heart', 'love health favorite'], ['star', 'favorite important'], ['bolt', 'energy fast idea'], + ['bulb', 'idea light think'], ['rocket', 'launch project start'], ['target', 'goal aim focus'], ['flag', 'milestone goal'], + ['calendar', 'date plan schedule'], ['clock', 'time'], ['bell', 'reminder notify'], ['bookmark', 'save later'], + ['board', 'kanban columns'], ['check2', 'done todo task'], ['inbox', 'capture tasks'], ['layers', 'stack design'], + ['file', 'document page'], ['folder', 'files directory'], ['tag', 'label'], ['hash', 'channel topic'], + ['terminal', 'shell console dev'], ['database', 'data storage sql'], ['cpu', 'chip hardware computer'], ['gamepad', 'game play controller'], + ['palette', 'art design color'], ['camera', 'photo picture'], ['image', 'picture photo'], ['music', 'song audio'], + ['chart', 'stats graph data'], ['wallet', 'money finance budget'], ['briefcase', 'work job business'], ['school', 'study university education'], + ['user', 'person me profile'], ['users', 'people team group'], ['mail', 'email letter'], ['chat', 'message talk'], + ['globe', 'world web internet'], ['map', 'travel place'], ['send', 'share plane'], ['tree', 'nature forest'], + ['sun', 'day light summer'], ['pulse', 'health fitness activity'], ['food', 'recipe cooking eat'], ['cart', 'shopping buy'], + ['gift', 'present birthday'], ['lock', 'private secure'], ['key', 'password access'], ['trash', 'delete bin'] +]; + +// Emoji offered in the picker, as "emoji words" pairs. +export const PICKER_EMOJI = [ + '๐Ÿ“ note memo write', '๐Ÿ“’ notebook ledger', '๐Ÿ“š books library study', '๐Ÿ“– book read', '๐Ÿ—‚๏ธ folders files', '๐Ÿ“ folder', '๐Ÿ“Œ pin', '๐Ÿ“Ž clip attach', + 'โœ… done check', 'โ˜‘๏ธ checkbox todo', '๐Ÿ—’๏ธ notepad', '๐Ÿ“… calendar date', '๐Ÿ—“๏ธ calendar plan', 'โฐ alarm time', 'โŒ› hourglass time', '๐Ÿ”– bookmark', + '๐Ÿ’ก idea bulb', '๐ŸŽฏ goal target', '๐Ÿš€ rocket launch', 'โญ star', 'โœจ sparkles new', '๐Ÿ”ฅ fire hot', 'โšก lightning fast', '๐Ÿ finish flag', + '๐Ÿ’ป laptop computer code', '๐Ÿ–ฅ๏ธ desktop computer', 'โŒจ๏ธ keyboard', '๐Ÿ–ฑ๏ธ mouse', '๐Ÿง  brain think', '๐Ÿค– robot ai', '๐Ÿงช test experiment', '๐Ÿ”ฌ science research', + '๐ŸŽฎ game controller', '๐Ÿ•น๏ธ joystick game', '๐Ÿ‘พ alien game', '๐Ÿงฉ puzzle', '๐ŸŽฒ dice game', 'โ™Ÿ๏ธ chess strategy', '๐ŸŽจ art paint', '๐ŸŽต music note', + '๐ŸŽง headphones music', '๐ŸŽฌ film movie', '๐Ÿ“ท camera photo', '๐ŸŽค microphone podcast', '๐ŸŽธ guitar', '๐Ÿ“บ tv', '๐Ÿ“ป radio', '๐ŸŽญ theater', + '๐Ÿ  home house', '๐Ÿข office building work', '๐Ÿซ school', '๐Ÿฅ hospital health', '๐ŸŒ world earth globe', '๐Ÿ—บ๏ธ map travel', 'โœˆ๏ธ plane travel flight', '๐Ÿš— car', + '๐Ÿšฒ bike', 'โ›ฐ๏ธ mountain', '๐Ÿ–๏ธ beach holiday', '๐Ÿ•๏ธ camping', '๐ŸŒฒ tree forest', '๐ŸŒฑ seedling grow', '๐ŸŒธ flower blossom', '๐Ÿ€ clover luck', + 'โ˜€๏ธ sun', '๐ŸŒ™ moon night', 'โ›… weather cloud', '๐ŸŒง๏ธ rain', 'โ„๏ธ snow winter', '๐ŸŒˆ rainbow', '๐ŸŒŠ wave ocean', '๐ŸŒ‹ volcano', + 'โ˜• coffee', '๐Ÿต tea', '๐Ÿ• pizza food', '๐Ÿ” burger food', '๐Ÿฅ— salad healthy', '๐ŸŽ apple fruit', '๐Ÿฐ cake dessert', '๐Ÿณ cooking recipe', + '๐Ÿ’ฐ money', '๐Ÿ’ณ card payment', '๐Ÿ“ˆ chart growth up', '๐Ÿ“‰ chart down', '๐Ÿ“Š bar chart stats', '๐Ÿงพ receipt bill', '๐Ÿฆ bank', '๐Ÿ’ผ briefcase work', + '๐Ÿ‘ค person user', '๐Ÿ‘ฅ people team', '๐Ÿ‘‹ wave hello', '๐Ÿค handshake deal', '๐Ÿ’ฌ chat message', '๐Ÿ“ง email mail', '๐Ÿ“ž phone call', '๐Ÿ“ฃ announcement', + 'โค๏ธ heart love', '๐Ÿ’ช strong fitness', '๐Ÿƒ run exercise', '๐Ÿง˜ yoga calm', '๐Ÿ˜ด sleep', '๐Ÿฉบ doctor health', '๐Ÿ’Š pill medicine', '๐Ÿ‹๏ธ gym', + '๐Ÿ”’ lock private', '๐Ÿ”‘ key password', '๐Ÿ› ๏ธ tools build', '๐Ÿ”ง wrench fix', 'โš™๏ธ gear settings', '๐Ÿงฐ toolbox', '๐Ÿ› bug', '๐Ÿงน cleanup', + '๐Ÿ“ฆ package box', '๐ŸŽ gift', '๐Ÿ›’ shopping cart', '๐Ÿท๏ธ tag label', '๐Ÿ”” bell reminder', 'โ“ question', 'โ— important', 'โš ๏ธ warning', + '๐Ÿถ dog', '๐Ÿฑ cat', '๐ŸฆŠ fox', '๐Ÿผ panda', '๐Ÿฆ‰ owl', '๐Ÿข turtle', '๐Ÿฆ‹ butterfly', '๐Ÿ™ octopus' +].map((s) => { const i = s.indexOf(' '); return [s.slice(0, i), s.slice(i + 1)]; }); + +export const isEmoji = (s) => typeof s === 'string' && /\p{Extended_Pictographic}/u.test(s) && s.length <= 16; + +export const paths = (name) => (PATHS[name] || PATHS.pen).split('|'); + +// String form for the vanilla editor engine. +export const ic = (name, cls = '') => + ``; + +export const PAGE_ICONS = ['sparkle', 'pen', 'leaf', 'book', 'code', 'moon', 'compass', 'cup']; + +export const COVERS = { + blush: ['#d99a91', '#caa07c'], + sage: ['#97b096', '#d1b48e'], + sand: ['#d9b886', '#c9958c'], + dusk: ['#a596c6', '#d59b9b'] +}; diff --git a/src/lib/markdown.js b/src/lib/markdown.js new file mode 100644 index 0000000..a81c05c --- /dev/null +++ b/src/lib/markdown.js @@ -0,0 +1,453 @@ +// Markdown <-> editor blocks. +// +// file body โ”€โ”€parseMarkdownโ”€โ”€> [{type, html, ...}] โ”€โ”€editorโ”€โ”€> blocks โ”€โ”€toMarkdownโ”€โ”€> file body +// +// Block types: p, h1, h2, h3, bullet, number, todo, quote, callout, toggle, code, divider, image, table. +// Inline formatting kept: **bold**, *italic*, ~~strike~~, ==highlight==, `code`, , [[page links]], [web](links), +// bare URLs, and footnote references [^id] / ^[inline note]. Footnote texts ("[^id]: โ€ฆ" lines) are blocks. +import { esc, unesc } from './text.js'; +import { linkLabel, IMAGE_FILE, OTHER_FILE } from './links.js'; + +let seq = 0; +export const blockId = () => 'b' + (++seq).toString(36) + Math.random().toString(36).slice(2, 6); +export const block = (type, html = '', extra = {}) => ({ id: blockId(), type, html, ...extra }); + +/* ---------- inline ---------- */ + +// A page link in the editor: one atomic chip. `ref` and `label` are plain text. +export const linkChip = (ref, label) => + `${esc(label || linkLabel(ref))}`; + +// Web links. Only these schemes become clickable; anything else stays as written. +export const isWebUrl = (u) => /^(https?:\/\/[^\s]+|mailto:[^\s]+)$/i.test(String(u || '')); + +// A web link in the editor. `url` and `title` are plain text, `label` is HTML. +// `auto` marks a bare URL in the file, so it is written back bare. +export const webLink = (url, label, { auto = false, title = '' } = {}) => + `${label}`; + +// Trailing punctuation is usually the sentence, not the URL; a ")" only belongs to it when it opened one. +function trimUrl(u) { + let url = u; + for (;;) { + const m = url.match(/(?:[.,;:!?'"*_]|"|')$/); + if (m) { url = url.slice(0, -m[0].length); continue; } + if (url.endsWith(')') && (url.match(/\(/g) || []).length < (url.match(/\)/g) || []).length) { url = url.slice(0, -1); continue; } + return url; + } +} + +// A footnote reference in the editor: an atomic superscript. The number is filled in by the editor (it depends on +// the order of references on the page); the file only ever stores [^id] or ^[text]. +export const footnoteRef = (id) => `${esc(id)}`; +export const inlineNote = (text) => `*`; + +export function mdToHtml(md) { + const held = []; + const hold = (html) => `\u0000${held.push(html) - 1}\u0000`; + let s = esc(md) + .replace(/`([^`\n]+)`/g, (_, c) => hold(`${c}`)) + .replace(/\$\$([^$\n]+)\$\$/g, (_, c) => hold(`${c}`)) + .replace(/\$([^$\n]+)\$/g, (_, c) => hold(`${c}`)) + .replace(/(? hold(linkChip(unesc(ref), alias ? unesc(alias) : ''))) + // footnotes: [^id] points at a "[^id]: text" line; ^[text] carries its text inline + .replace(/\[\^([^\]\s^]+)\]/g, (_, id) => hold(footnoteRef(unesc(id)))) + .replace(/\^\[([^\]\n]+)\]/g, (_, t) => hold(inlineNote(unesc(t)))) + // [label](url "title") and [label]() + .replace(/(? { + const url = unesc(href.replace(/^<|>$/g, '')); + return isWebUrl(url) ? hold(webLink(url, mdToHtml(unesc(label)), { title: title ? unesc(title) : '' })) : m; + }) + // bare URLs + .replace(/(^|[\s(\u0000])((?:https?:\/\/)[^\s<\u0000]+)/g, (m, pre, raw) => { + const url = trimUrl(raw); + if (url.length < 9) return m; + return pre + hold(webLink(unesc(url), url, { auto: true })) + raw.slice(url.length); + }); + s = s + .replace(/<kbd>(.*?)<\/kbd>/g, '$1') + .replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, '$1') + .replace(/~~(?=\S)([\s\S]*?\S)~~/g, '$1') + .replace(/(?$1') + .replace(/(^|[^*\w])\*(?=[^\s*])([^*\n]*?[^\s*])\*(?!\*)/g, '$1$2') + .replace(/(^|[^_\w])_(?=\S)([^_\n]*?\S)_(?!\w)/g, '$1$2') + .replace(/\u0000(\d+)\u0000/g, (_, i) => held[+i]); + return s; +} + + +export function htmlToMd(html) { + if (!html) return ''; + const s = String(html) + .replace(/(<\/(?:span|sup)>)(?: |\u00a0)$/, '$1') // the spacer typed after a link or footnote at the end of a line + .replace(/]*)>([\s\S]*?)<\/sup>/gi, (_, attrs, inner) => { + const fn = attrs.match(/\bdata-fn="([^"]*)"/); + if (fn) return esc(`[^${unesc(fn[1])}]`); + const note = attrs.match(/\bdata-inline="([^"]*)"/); + return note ? esc(`^[${unesc(note[1])}]`) : inner; + }) + .replace(/]*\bdata-ref="([^"]*)"[^>]*)>([\s\S]*?)<\/span>/gi, (_, _a, ref, label) => { + const r = unesc(ref), l = unesc(label.replace(/<[^>]+>/g, '')); + return esc(l && l !== linkLabel(r) ? `[[${r}|${l}]]` : `[[${r}]]`); + }) + .replace(/]*)>([\s\S]*?)<\/a>/gi, (m, attrs, inner) => { + const attr = (name) => { const x = attrs.match(new RegExp(`\\b${name}="([^"]*)"`, 'i')); return x ? unesc(x[1]) : null; }; + const href = attr('href'); + const text = htmlToMd(inner); + if (!href || !isWebUrl(href)) return esc(text); + if (attr('data-auto') && text === href) return esc(href); + const title = attr('data-title'); + const target = /[\s()<>]/.test(href) ? `<${href}>` : href; + return esc(`[${text || href}](${target}${title ? ` "${title}"` : ''})`); + }) + .replace(//gi, '\n') + .replace(/<(div|p)(\s[^>]*)?>/gi, '\n') + .replace(/<\/(div|p)>/gi, '') + .replace(/]*>([\s\S]*?)<\/kbd>/gi, '\u0001$1\u0002') + .replace(/]*)>([\s\S]*?)<\/code>/gi, (_, attrs, x) => { + const text = x.replace(/<[^>]+>/g, ''); + if (/class=["']?[^"']*\blanguage-math\b[^"']*/i.test(attrs)) return '$' + text + '$'; + return '`' + text + '`'; + }) + .replace(/<(b|strong)(\s[^>]*)?>([\s\S]*?)<\/\1>/gi, (_, _t, _a, x) => (x.trim() ? `**${x}**` : x)) + .replace(/<(i|em)(\s[^>]*)?>([\s\S]*?)<\/\1>/gi, (_, _t, _a, x) => (x.trim() ? `*${x}*` : x)) + .replace(/<(s|strike|del)(\s[^>]*)?>([\s\S]*?)<\/\1>/gi, (_, _t, _a, x) => (x.trim() ? `~~${x}~~` : x)) + .replace(/]*)?>([\s\S]*?)<\/mark>/gi, (_, _a, x) => (x.trim() ? `==${x}==` : x)) + .replace(/<[^>]+>/g, ''); + return unesc(s) + .replace(/\u0001/g, '') + .replace(/\u0002/g, '') + .replace(/^\n+/, ''); +} + +/* ---------- blocks ---------- */ + +export function parseMarkdown(src) { + const lines = String(src || '').replace(/\r\n?/g, '\n').split('\n'); + const out = []; + let para = []; + const flush = () => { + if (para.length) out.push(block('p', mdToHtml(para.join('\n')))); + para = []; + }; + let i = 0; + while (i < lines.length) { + const line = lines[i]; + let m; + + + if ((m = line.match(/^\$\$\s*$/))) { + flush(); + const buf = []; + i++; + while (i < lines.length && !/^\$\$\s*$/.test(lines[i])) buf.push(lines[i++]); + i++; + out.push(block('math', '', { text: buf.join('\n') })); + continue; + } + if ((m = line.match(/^\`\`\`\s*mermaid\s*$/))) { + flush(); + const buf = []; + i++; + while (i < lines.length && !/^\`\`\`\s*$/.test(lines[i])) buf.push(lines[i++]); + i++; + out.push(block('mermaid', '', { text: buf.join('\n') })); + continue; + } + if ((m = line.match(/^```\s*([\w+#.-]*)\s*$/))) { + flush(); + const buf = []; + i++; + while (i < lines.length && !/^```\s*$/.test(lines[i])) buf.push(lines[i++]); + i++; + out.push(block('code', '', { lang: m[1] || 'txt', text: buf.join('\n') })); + continue; + } + if (!line.trim()) { flush(); i++; continue; } + if ((m = line.match(/^\[\^([^\]\s^]+)\]:[ \t]?(.*)$/))) { + flush(); + const text = [m[2]]; + i++; + while (i < lines.length && /^( {4}|\t)\S/.test(lines[i])) text.push(lines[i++].replace(/^( {4}|\t)/, '')); + out.push(block('footnote', mdToHtml(text.join('\n')), { fid: m[1] })); + continue; + } + if ((m = line.match(/^\s*!\[([^\]]*)\]\(\s*(?:<([^>]+)>|([^)\s]+))(?:\s+"[^"]*")?\s*\)\s*$/))) { + flush(); + const raw = m[2] || m[3]; + let src = raw; + try { src = decodeURI(raw); } catch { /* keep as written */ } + out.push(block('image', '', { src, alt: m[1] })); + i++; + continue; + } + if ((m = line.match(/^\s*!\[\[([^\]|]+)(?:\|([^\]]*))?\]\]\s*$/))) { + const target = m[1].trim(); + // ![[photo.png]] is a picture; ![[Page]] / ![[Page#Heading]] shows that note inline; + // other files (pdf, audio...) aren't supported, so they stay as plain text. + if (IMAGE_FILE.test(target)) { + flush(); + out.push(block('image', '', { src: target, alt: m[2] || '', wiki: true })); + i++; + continue; + } + if (!OTHER_FILE.test(target)) { + flush(); + out.push(block('embed', '', { ref: target, alias: m[2] || '' })); + i++; + continue; + } + } + if (/^(\*\*\*|---|___)\s*$/.test(line)) { flush(); out.push(block('divider')); i++; continue; } + if ((m = line.match(/^(#{1,6})\s+(.*?)\s*#*\s*$/))) { + flush(); + out.push(block('h' + Math.min(3, m[1].length), mdToHtml(m[2]))); + i++; + continue; + } + if ((m = line.match(/^([ \t]*)[-*+]\s+\[([ xX])\]\s?(.*)$/))) { + flush(); + out.push(block('todo', mdToHtml(m[3]), { checked: m[2] !== ' ', indent: Math.floor(m[1].replace(/\t/g, ' ').length / 2) })); + i++; + continue; + } + if ((m = line.match(/^([ \t]*)[-*+]\s+(.*)$/))) { + flush(); + out.push(block('bullet', mdToHtml(m[2]), { indent: Math.floor(m[1].replace(/\t/g, ' ').length / 2) })); + i++; + continue; + } + if ((m = line.match(/^([ \t]*)\d{1,9}[.)]\s+(.*)$/))) { + flush(); + out.push(block('number', mdToHtml(m[2]), { indent: Math.floor(m[1].replace(/\t/g, ' ').length / 2) })); + i++; + continue; + } + const cm = line.match(/^>\s?\[!(\w+)\]/); + if (cm) { + flush(); + const buf = []; + const first = line.replace(/^>\s?\[!\w+\][-+]?\s*/, ''); + if (first) buf.push(first); + i++; + while (i < lines.length && /^>/.test(lines[i])) buf.push(lines[i++].replace(/^>\s?/, '')); + out.push(block('callout', mdToHtml(buf.join('\n').trim()), { kind: cm[1].toLowerCase() })); + continue; + } + if (/^>/.test(line)) { + flush(); + const buf = []; + while (i < lines.length && /^>/.test(lines[i])) buf.push(lines[i++].replace(/^>\s?/, '')); + out.push(block('quote', mdToHtml(buf.join('\n')))); + continue; + } + if (/^]*)?>\s*$/i.test(line.trim())) { + flush(); + i++; + let summary = ''; + const sm = i < lines.length && lines[i].match(/^\s*(.*)<\/summary>\s*$/i); + if (sm) { summary = sm[1]; i++; } + const body = []; + while (i < lines.length && !/^\s*<\/details>\s*$/i.test(lines[i])) body.push(lines[i++]); + i++; + out.push(block('toggle', mdToHtml(summary), { body: mdToHtml(body.join('\n').trim()), open: false })); + continue; + } + if (line.includes('|') && i + 1 < lines.length && isDelimRow(lines[i + 1]) && splitRow(line).length === splitRow(lines[i + 1]).length) { + flush(); + const head = splitRow(line); + const align = splitRow(lines[i + 1]).map((d) => (d.startsWith(':') && d.endsWith(':') ? 'center' : d.endsWith(':') ? 'right' : d.startsWith(':') ? 'left' : null)); + const src = [line, lines[i + 1]]; + const body = []; + i += 2; + while (i < lines.length && lines[i].includes('|') && lines[i].trim()) { + const cells = splitRow(lines[i]); + body.push(head.map((_, c) => cells[c] ?? '')); + src.push(lines[i++]); + } + const cell = (t) => mdToHtml(t.replace(/\\\|/g, '|')); + out.push(block('table', '', { rows: [head, ...body].map((r) => r.map(cell)), align, src: src.join('\n') })); + continue; + } + para.push(line); + i++; + } + flush(); + if (!out.length) out.push(block('p')); + return out; +} + +// Spaces and brackets would end the link early; everything else stays readable. +const imagePath = (src) => String(src || '').replace(/[ ()<>]/g, encodeURIComponent); + +const LISTS = new Set(['bullet', 'number', 'todo']); + +/* tables (GitHub style): | a | b |, then | --- | :-: |, then rows */ +const isDelimRow = (l) => /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(l) && l.includes('-'); +// Cells of one table row. Pipes inside `code` or escaped as \| don't split. +function splitRow(line) { + let s = line.trim(); + if (s.startsWith('|')) s = s.slice(1); + if (s.endsWith('|') && !s.endsWith('\\|')) s = s.slice(0, -1); + const cells = []; + let cur = '', code = false; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (ch === '\\' && s[i + 1] === '|') { cur += '\\|'; i++; continue; } + if (ch === '`') code = !code; + if (ch === '|' && !code) { cells.push(cur.trim()); cur = ''; continue; } + cur += ch; + } + cells.push(cur.trim()); + return cells; +} +// An edited table is written back with its columns padded to line up. +function tableToMd(b) { + const rows = (b.rows || [[]]).map((r) => r.map((h) => htmlToMd(h).replace(/\n/g, ' ').replace(/\|/g, '\\|').trim())); + const cols = Math.max(1, ...rows.map((r) => r.length)); + const width = Array.from({ length: cols }, (_, c) => Math.max(3, ...rows.map((r) => (r[c] || '').length))); + const line = (r) => '| ' + width.map((w, c) => (r[c] || '').padEnd(w)).join(' | ') + ' |'; + const align = b.align || []; + const delim = '| ' + width.map((w, c) => { + const a = align[c]; + return a === 'center' ? ':' + '-'.repeat(w - 2) + ':' : a === 'right' ? '-'.repeat(w - 1) + ':' : a === 'left' ? ':' + '-'.repeat(w - 1) : '-'.repeat(w); + }).join(' | ') + ' |'; + return [line(rows[0] || []), delim, ...rows.slice(1).map(line)].join('\n'); +} + +const quoteLines = (t, prefix = '> ') => t.split('\n').map((l) => (l ? prefix + l : prefix.trimEnd())).join('\n'); + +export function blockToMd(b, n = 1) { + const t = htmlToMd(b.html); + switch (b.type) { + case 'h1': return '# ' + t; + case 'h2': return '## ' + t; + case 'h3': return '### ' + t; + case 'bullet': return '- ' + t; + case 'number': return `${n}. ` + t; + case 'todo': return `- [${b.checked ? 'x' : ' '}] ` + t; + case 'quote': return quoteLines(t); + case 'callout': return `> [!${b.kind || 'note'}]\n` + quoteLines(t); + case 'divider': return '---'; + case 'code': return '```' + (b.lang || '') + '\n' + (b.text || '') + '\n```'; + case 'math': return '$$\n' + (b.text || '') + '\n$$'; + case 'mermaid': return '```mermaid\n' + (b.text || '') + '\n```'; + case 'table': return b.src ?? tableToMd(b); + case 'footnote': return `[^${b.fid}]: ` + t.replace(/\n/g, '\n '); + case 'embed': return `![[${b.ref}${b.alias ? '|' + b.alias : ''}]]`; + case 'image': return b.wiki ? `![[${b.src}${b.alt ? '|' + b.alt : ''}]]` : `![${b.alt || ''}](${imagePath(b.src)})`; + case 'toggle': return `
\n${t}\n\n${htmlToMd(b.body)}\n\n
`; + default: return t; + } +} + +export function toMarkdown(blocks) { + let out = ''; + let prevList = false; + let prevType = null; + let n = 0; // position in a run of numbered items + for (const b of blocks) { + if (b.type === 'p' && !htmlToMd(b.html).trim()) continue; + const isList = LISTS.has(b.type); + n = b.type === 'number' ? n + 1 : 0; + let md = blockToMd(b, n); + if (isList && b.indent) { + const sp = ' '.repeat(b.indent); + md = sp + md.replace(/\n/g, '\n' + sp); + } + const tight = (isList && prevList) || (b.type === 'footnote' && prevType === 'footnote'); + out += out ? (tight ? '\n' : '\n\n') : ''; + out += md; + prevList = isList; + prevType = b.type; + } + return out ? out + '\n' : ''; +} + +export const blockText = (b) => + b.type === 'embed' ? '' : b.type === 'table' ? (b.rows || []).flat().map(htmlToMd).join(' ') : b.type === 'code' || b.type === 'math' || b.type === 'mermaid' ? b.text || '' : b.type === 'divider' || b.type === 'image' ? '' :b.type === 'toggle' ? htmlToMd(b.html) + ' ' + htmlToMd(b.body) : htmlToMd(b.html); + +/* ---------- HTML export ---------- */ + +// Footnote numbers in reading order: each [^id] gets a number the first time it's referenced, +// each ^[inline note] its own. Returns { refs: Map(id -> n), html(s) } where html() rewrites references for export. +export function numberFootnotes(blocks) { + const refs = new Map(); + let n = 0; + const html = (s) => String(s || '').replace(/]*)>[\s\S]*?<\/sup>/gi, (m, attrs) => { + const fn = attrs.match(/\bdata-fn="([^"]*)"/); + if (fn) { + if (!refs.has(fn[1])) refs.set(fn[1], ++n); + return `${refs.get(fn[1])}`; + } + const note = attrs.match(/\bdata-inline="([^"]*)"/); + return note ? `${++n}` : m; + }); + return { refs, html }; +} + +export function toHtmlDocument(title, raw) { + const fns = numberFootnotes(raw); + const blocks = raw.map((b) => ({ ...b, html: fns.html(b.html), body: b.body && fns.html(b.body), rows: b.rows && b.rows.map((r) => r.map(fns.html)) })); + const body = []; + let list = null; // 'ul' or 'ol' while inside a list + for (const b of blocks) { + const want = b.type === 'number' ? 'ol' : LISTS.has(b.type) ? 'ul' : null; + if (list && list !== want) { body.push(``); list = null; } + if (want && !list) { body.push(`<${want}>`); list = want; } + switch (b.type) { + case 'h1': case 'h2': case 'h3': body.push(`<${b.type}>${b.html}`); break; + case 'bullet': case 'number': body.push(`
  • ${b.html}
  • `); break; + case 'todo': body.push(`
  • ${b.html}
  • `); break; + case 'quote': body.push(`
    ${b.html}
    `); break; + case 'callout': body.push(``); break; + case 'divider': body.push('
    '); break; + case 'table': { + const align = (c) => (b.align?.[c] ? ` style="text-align:${b.align[c]}"` : ''); + const [head = [], ...rest] = b.rows || []; + body.push(`${head.map((h, c) => `${h}`).join('')}${rest.map((r) => `${r.map((h, c) => `${h}`).join('')}`).join('')}
    `); + break; + } + case 'footnote': body.push(`

    ${fns.refs.get(esc(b.fid)) ?? esc(b.fid)} ${b.html}

    `); break; + case 'embed': body.push(`
    Embedded: ${esc(b.ref)}
    `); break; + case 'image': body.push(`${esc(b.alt || '')}`); break; + case 'code': body.push(`
    ${esc(b.text || '')}
    `); break; + case 'math': body.push(`
    ${esc(b.text || '')}
    `); break; + case 'mermaid': body.push(`
    ${esc(b.text || '')}
    `); break; + case 'toggle': body.push(`
    ${b.html}

    ${b.body || ''}

    `); break; + default: if (b.html) body.push(`

    ${b.html}

    `); + } + } + if (list) body.push(``); + return ` + + + +${esc(title)} + + + +
    +

    ${esc(title)}

    +${body.join('\n')} +
    + + +`; +}