diff --git a/src/app.css b/src/app.css index dc1f3d9..b10ca7b 100644 --- a/src/app.css +++ b/src/app.css @@ -4,6 +4,8 @@ --mark:rgba(250,204,21,.26); /* ==highlight== */ --fs:16px; /* page text size (Settings) */ --page-w:740px; /* page width (Settings) */ + --f-page:var(--f-ui); /* page font (Settings) */ + --lh:1.7; /* page line spacing (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); @@ -32,6 +34,29 @@ --grain:.07; color-scheme:light; } +/* color themes (Settings): surfaces and text per palette, in both light and dark mode; the default is defined above */ +:root:not([data-theme="light"])[data-palette="midnight"]{--ground:#0a0e17;--island:#0f1522;--island-2:#151c2c;--raise:#1b2336;--ink:#e6eaf2;--ink-2:#9aa5ba;--ink-3:#677188;--code-bg:#151c2c} +:root[data-theme="light"][data-palette="midnight"]{--ground:#dfe5ee;--island:#f7f9fc;--island-2:#eef2f8;--raise:#ffffff;--ink:#17202e;--ink-2:#52607a;--ink-3:#92a0b8;--code-bg:#eef2f8} +:root:not([data-theme="light"])[data-palette="nord"]{--ground:#232831;--island:#2b303b;--island-2:#313745;--raise:#3a4150;--ink:#e5e9f0;--ink-2:#a9b3c4;--ink-3:#7b8699;--code-bg:#313745} +:root[data-theme="light"][data-palette="nord"]{--ground:#dde2ea;--island:#eceff4;--island-2:#e5e9f0;--raise:#ffffff;--ink:#2e3440;--ink-2:#4c566a;--ink-3:#8892a4;--code-bg:#e5e9f0} +:root:not([data-theme="light"])[data-palette="forest"]{--ground:#0c120f;--island:#121a15;--island-2:#18211b;--raise:#1e2922;--ink:#e4ece6;--ink-2:#9aab9f;--ink-3:#68786d;--code-bg:#18211b} +:root[data-theme="light"][data-palette="forest"]{--ground:#dfe6df;--island:#f6f9f5;--island-2:#edf2ec;--raise:#ffffff;--ink:#1a241d;--ink-2:#506055;--ink-3:#93a298;--code-bg:#edf2ec} +:root:not([data-theme="light"])[data-palette="sepia"]{--ground:#17130e;--island:#1e1912;--island-2:#251f17;--raise:#2d261c;--ink:#ece4d6;--ink-2:#b0a48f;--ink-3:#7a705f;--code-bg:#251f17} +:root[data-theme="light"][data-palette="sepia"]{--ground:#e6dccb;--island:#f8f1e3;--island-2:#f0e7d4;--raise:#fffaf0;--ink:#3a2f20;--ink-2:#6a5b45;--ink-3:#9d8e75;--code-bg:#f0e7d4} +:root:not([data-theme="light"])[data-palette="catppuccin"]{--ground:#11111b;--island:#181825;--island-2:#1e1e2e;--raise:#313244;--ink:#cdd6f4;--ink-2:#a6adc8;--ink-3:#6c7086;--code-bg:#11111b} +:root[data-theme="light"][data-palette="catppuccin"],:root[data-theme="light"][data-palette="catppuccin-dark"]{--ground:#dce0e8;--island:#eff1f5;--island-2:#e6e9ef;--raise:#ffffff;--ink:#4c4f69;--ink-2:#6c6f85;--ink-3:#9ca0b0;--code-bg:#e6e9ef} +:root:not([data-theme="light"])[data-palette="catppuccin-dark"]{--ground:#08080d;--island:#0d0d14;--island-2:#13131c;--raise:#232333;--ink:#cdd6f4;--ink-2:#a6adc8;--ink-3:#6c7086;--code-bg:#08080d} +/* AMOLED is a dark-only theme: in light mode it looks like Default */ +:root:not([data-theme="light"])[data-palette="amoled"]{--ground:#000;--island:#000;--island-2:#0b0b0c;--raise:#161617;--line:rgba(255,255,255,.1);--code-bg:#0b0b0c} +/* accent colors (Settings); the default is blush, defined above */ +:root:not([data-theme="light"])[data-accent="amber"]{--blush:#e0b672;--blush-ink:#2a2012;--blush-soft:rgba(224,182,114,.13);--blush-line:rgba(224,182,114,.4)} +:root[data-theme="light"][data-accent="amber"]{--blush:#a8741c;--blush-ink:#fffaf0;--blush-soft:rgba(168,116,28,.11);--blush-line:rgba(168,116,28,.4)} +:root:not([data-theme="light"])[data-accent="sage"]{--blush:#a3bf9c;--blush-ink:#172314;--blush-soft:rgba(163,191,156,.13);--blush-line:rgba(163,191,156,.4)} +:root[data-theme="light"][data-accent="sage"]{--blush:#5a8a52;--blush-ink:#f6fff4;--blush-soft:rgba(90,138,82,.11);--blush-line:rgba(90,138,82,.4)} +:root:not([data-theme="light"])[data-accent="sky"]{--blush:#8fb6e0;--blush-ink:#101e2e;--blush-soft:rgba(143,182,224,.13);--blush-line:rgba(143,182,224,.4)} +:root[data-theme="light"][data-accent="sky"]{--blush:#3f73b0;--blush-ink:#f5faff;--blush-soft:rgba(63,115,176,.11);--blush-line:rgba(63,115,176,.4)} +:root:not([data-theme="light"])[data-accent="violet"]{--blush:#b3a1dc;--blush-ink:#1c1630;--blush-soft:rgba(179,161,220,.13);--blush-line:rgba(179,161,220,.4)} +:root[data-theme="light"][data-accent="violet"]{--blush:#7a62b8;--blush-ink:#fbf8ff;--blush-soft:rgba(122,98,184,.11);--blush-line:rgba(122,98,184,.4)} *{box-sizing:border-box} /* Scrollbars: thin, no arrows, and only visible while the scrolling area is hovered. */ ::-webkit-scrollbar{width:10px;height:10px} @@ -46,6 +71,10 @@ body{margin:0;background:var(--ground);color:var(--ink);font:13.5px/1.5 var(--f- .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)} +/* while a drag-selection crosses blocks, the editor sets the selection itself and paints it with this highlight */ +body.xsel,body.xsel *{user-select:none!important} +body.xsel ::selection{background:transparent!important} +::highlight(xsel){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} @@ -53,22 +82,46 @@ button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:poin 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{height:100%;display:grid;grid-template-columns:var(--side-w,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} .app.focus-mode{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)} +/* Zen-style: the sidebar sits directly on the ground, only the page is an island */ +.app > .sidebar.island{background:transparent;box-shadow:none;border-radius:0} +.app{gap:0} +.app > .main{margin-left:2px} +.app.collapsed > .main,.app.focus-mode > .main{margin-left:0} .app.collapsed .sidebar{opacity:0;pointer-events:none} +/* hover reveal: the hidden sidebar floats over the page (nothing underneath moves) while the mouse is at the edge */ +.edge-hot{position:fixed;left:0;top:44px;bottom:0;width:7px;z-index:50} +@media (min-width:761px){ + .app > .main{grid-column:2} + /* a hidden sidebar stays in place, just invisible, so it can fade out as well as in */ + .app.collapsed:not(.focus-mode) .sidebar{position:fixed;left:10px;top:10px;bottom:10px;width:var(--side-w,268px);z-index:60;transform:translateX(-14px);visibility:hidden;background:var(--island);border-radius:12px;box-shadow:0 0 0 1px var(--line-2),0 18px 50px -12px rgba(0,0,0,.55);transition:opacity .2s var(--ease),transform .26s var(--ease),visibility 0s linear .26s} + .app.collapsed.peek .sidebar{opacity:1;pointer-events:auto;transform:none;visibility:visible;transition:opacity .2s var(--ease),transform .26s var(--ease),visibility 0s} +} .app.focus-mode .sidebar{opacity:0;pointer-events:none} .app.focus-mode .topbar { opacity: 0; - pointer-events: none; transition: opacity 0.4s ease; + pointer-events: none; +} +/* We need a hit area. We can use a pseudo-element on the main area to capture hover, or a generic hit area div. + Actually, the simplest approach: pointer-events: none on the topbar, but add a 20px invisible div at the top of main that on hover shows the topbar. + Since we don't have that div, we can just use CSS: +*/ +.app.focus-mode .topbar::before { + content: ""; + position: absolute; + top: 10px; left: 0; right: 0; height: 30px; z-index: -1; + pointer-events: auto; } .app.focus-mode .topbar:hover, .app.focus-mode .topbar:focus-within { opacity: 1; pointer-events: auto; } + .app.focus-mode .main { background: var(--island); } @@ -83,7 +136,7 @@ kbd{font:500 10.5px/1 var(--f-mono);padding:3px 5px;border-radius:4px;background z-index: 20; } -.sb-inner{width:268px;height:100%;display:flex;flex-direction:column;padding:10px;gap:2px} +.sb-inner{width:100%;min-width:220px;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)} @@ -94,6 +147,10 @@ kbd{font:500 10.5px/1 var(--f-mono);padding:3px 5px;border-radius:4px;background .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} +.qas{display:flex;flex-direction:column;gap:2px} +.qas.row{flex-direction:row;gap:4px;margin:2px 0 0} +.qas.row .qa{flex:1;justify-content:center;gap:0;height:36px;padding:0} +.qas.row .qa-t,.qas.row .qa kbd{display:none} .qa:hover{background:var(--hover);color:var(--ink)} .qa:active{transform:scale(.98)} .qa kbd{margin-left:auto} @@ -132,12 +189,41 @@ kbd{font:500 10.5px/1 var(--f-mono);padding:3px 5px;border-radius:4px;background .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} +.sec-toggle{display:flex;align-items:center;gap:2px;margin:-3px 0 -3px -6px;padding:3px 6px 3px 2px;border-radius:6px;font:inherit;letter-spacing:inherit;text-transform:inherit;color:inherit;transition:background .15s,color .15s} +.sec-toggle:hover{background:var(--hover);color:var(--ink-2)} +.sec-toggle .chev{width:16px;height:16px;pointer-events:none} +.sec-toggle em{font-style:normal;margin-left:7px;font-weight:500;letter-spacing:0;color:var(--ink-3);opacity:.8;font-variant-numeric:tabular-nums} +.sec-active{height:22px;padding:0 6px 0 8px;font-size:12px;text-transform:none;letter-spacing:0;max-width:60%} +.sec-active .i{width:11px;height:11px;margin-left:3px;flex:none} +.tags-wrap{display:grid;grid-template-rows:0fr;transition:grid-template-rows .4s var(--ease)} +.tags-wrap.open{grid-template-rows:1fr} +.tags-in{overflow:hidden;min-height:0;opacity:0;transform:translateY(-6px);transition:opacity .22s,transform .4s var(--ease)} +.tags-wrap.open>.tags-in{opacity:1;transform:none;transition-delay:.05s,0s} +.tags-wrap.open .tag{animation:tagIn .34s var(--spring) both} +.tags-wrap.open .tag:nth-child(2){animation-delay:.03s}.tags-wrap.open .tag:nth-child(3){animation-delay:.06s} +.tags-wrap.open .tag:nth-child(4){animation-delay:.09s}.tags-wrap.open .tag:nth-child(n+5){animation-delay:.12s} +@keyframes tagIn{from{opacity:0;transform:translateY(-4px) scale(.94)}} +@media (prefers-reduced-motion:reduce){.tags-wrap,.tags-in{transition:none}.tags-wrap.open .tag{animation:none}} +.tag-find{display:flex;align-items:center;gap:6px;height:28px;margin:2px 6px 6px;padding:0 4px 0 9px;border-radius:7px;background:var(--hover);color:var(--ink-3)} +.tag-find:focus-within{box-shadow:inset 0 0 0 1.5px var(--blush-line)} +.tag-find .i{width:13px;height:13px;flex:none} +.tag-find input{flex:1;min-width:0;height:100%;border:0;outline:0;background:none;color:var(--ink);font:12.5px var(--f-ui)} +.tag-find .btn-icon{width:20px;height:20px} +.tag-find .btn-icon .i{width:11px;height:11px} +.tags{display:flex;flex-wrap:wrap;gap:5px;padding:2px 6px 8px;max-height:118px;overflow-y:auto;overscroll-behavior:contain} .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.tinted{background:color-mix(in srgb,var(--tc) 17%,var(--hover));color:var(--ink)} +.tag.tinted span{color:var(--tc)} +.tag.tinted:hover{background:color-mix(in srgb,var(--tc) 28%,var(--hover))} +.tag.tinted.on{background:var(--tc);color:#1d1713} +.tag.tinted.on span,.tag.tinted.on em{color:inherit;opacity:.65} +.chip.tinted{background:color-mix(in srgb,var(--tc) 20%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--tc) 38%,transparent)} +.tag-swatches{display:flex;justify-content:space-between;gap:4px;padding:8px 10px 6px} +.tag-swatches .cv-swatch{width:17px;height:17px;flex:none} .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} @@ -150,7 +236,12 @@ kbd{font:500 10.5px/1 var(--f-mono);padding:3px 5px;border-radius:4px;background .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)} +.nav-btns{display:flex;align-items:center;gap:1px;flex:none} +.nav-btn{width:28px;height:28px;display:grid;place-items:center;border-radius:6px;color:var(--ink-2);transition:background .15s,color .15s,transform .2s var(--spring)} +.nav-btn .i{width:15px;height:15px} +.nav-btn:hover:not(:disabled){background:var(--hover);color:var(--ink)} +.nav-btn:disabled{opacity:.26;cursor:default;pointer-events:none} +.app.collapsed .menu-btn, .app.focus-mode .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} @@ -174,13 +265,13 @@ kbd{font:500 10.5px/1 var(--f-mono);padding:3px 5px;border-radius:4px;background .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} +.page.enter .head{animation:rise .5s var(--ease) backwards} .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} +.i.emo{display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;white-space:nowrap;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} @@ -196,9 +287,9 @@ kbd{font:500 10.5px/1 var(--f-mono);padding:3px 5px;border-radius:4px;background .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-emo{position:relative;top:.04em;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{font-family:var(--f-page);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} @@ -224,6 +315,32 @@ kbd{font:500 10.5px/1 var(--f-mono);padding:3px 5px;border-radius:4px;background .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)} +/* columns: blocks side by side (editor.js layout()) */ +.cols{position:relative;display:grid;grid-template-columns:repeat(var(--n,2),minmax(0,1fr));gap:48px;margin:8px 0} +.cols>.col{min-width:0;position:relative} +.cols .block>.gutter{left:-28px} +.cols .block>.gutter .g-add{display:none} +/* between two columns: a line, and a handle to change their widths */ +.col-rs{position:absolute;left:-44px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:2;touch-action:none} +.col-rs::after{content:'';position:absolute;left:3px;top:2px;bottom:2px;width:2px;border-radius:2px;background:var(--line);opacity:0;transition:opacity .2s,background .15s} +.cols:hover .col-rs::after,.cols:focus-within .col-rs::after{opacity:1} +.col-rs:hover::after,body.col-resizing .col-rs::after{background:var(--blush);opacity:1} +.cols>.col>.col-rs{display:none} +.cols>.col~.col>.col-rs{display:block} +body.col-resizing,body.col-resizing *{cursor:col-resize!important;user-select:none!important} +.cols-bar{position:absolute;right:0;top:-26px;display:flex;gap:2px;opacity:0;transition:opacity .15s;z-index:3} +.cols:hover>.cols-bar,.cols:focus-within>.cols-bar{opacity:1} +.cols-bar button{width:24px;height:24px;display:grid;place-items:center;border-radius:6px;color:var(--ink-3)} +.cols-bar button:hover{background:var(--hover);color:var(--ink)} +.cols-bar .i{width:13px;height:13px} +.cols.full .cols-add{display:none} /* three columns at most */ +.cols .b-h1{margin-top:6px}.cols .b-h2{margin-top:4px}.cols .b-h3{margin-top:2px} +/* dropping beside something (a new column): an upright marker */ +.drop.v{width:3px;height:40px} +@media (max-width:640px){.cols{grid-template-columns:minmax(0,1fr)!important;gap:4px}.col-rs{display:none}} +/* the same, read-only (embeds, canvas cards, previews) */ +.eb-cols{display:grid;grid-template-columns:repeat(var(--n,2),minmax(0,1fr));gap:20px;margin:4px 0} +.eb-col{min-width:0} .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} @@ -236,9 +353,17 @@ body.block-dragging,body.block-dragging *{cursor:grabbing!important;user-select: .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,.c2{flex:1;min-width:0;outline:none;font-family:var(--f-page);font-size:var(--fs);line-height:var(--lh);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:""} +/* formatting shown as its Markdown source while the caret is in it (editor.js: live preview) */ +.md-src[data-fmt=b]{font-weight:700} +.md-src[data-fmt=bi]{font-weight:700;font-style:italic} +.md-src[data-fmt=i]{font-style:italic} +.md-src[data-fmt=s]{text-decoration:line-through;text-decoration-color:var(--ink-3)} +.md-src[data-fmt=mark]{background:var(--mark);border-radius:3px} +.md-src[data-fmt=code]{font:500 .84em var(--f-mono);background:var(--hover);color:var(--blush);padding:.12em .1em;border-radius:4px} +.md-mk{color:var(--ink-3);font-weight:400;font-style:normal;text-decoration:none} .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} @@ -254,8 +379,10 @@ body.block-dragging,body.block-dragging *{cursor:grabbing!important;user-select: .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)} +/* the strike is only there on a checked item: an always-present transparent line-through gets painted by Chromium + whenever a spelling squiggle sits on the word */ +.b-todo .c{transition:color .3s} +.b-todo.done .c{color:var(--ink-3);text-decoration:line-through;text-decoration-color:var(--ink-3);text-decoration-thickness:1.5px} .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} @@ -279,7 +406,7 @@ body.block-dragging,body.block-dragging *{cursor:grabbing!important;user-select: .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} +pre.c{margin:0;padding:6px 18px 16px;font:calc(var(--fs) * .84 * var(--code-scale,1))/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} @@ -325,42 +452,61 @@ pre.c{margin:0;padding:6px 18px 16px;font:calc(var(--fs) * .84)/1.7 var(--f-mono .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} +.board{overflow-x:auto;overflow-y:hidden;padding:6px var(--side) 16px;margin-top:10px;animation:dbSlide .28s var(--ease) backwards} .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} +.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:box-shadow .2s var(--ease),background .2s var(--ease),transform .25s var(--spring),opacity .2s} +.kcol.over{background:var(--blush-soft);box-shadow:0 0 0 1.5px var(--blush-line);animation:dbRing .6s var(--ease)} +.kcol.colover{box-shadow:inset 3px 0 0 var(--blush),0 0 0 1px var(--line)} +.kcol.lifted{opacity:.35;transform:scale(.98) rotate(.6deg)} .khead{display:flex;align-items:center;gap:2px;padding:4px 4px 6px 6px;cursor:grab;min-height:38px} +.khead:active{cursor:grabbing} .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} +.kh-title:active .db-opt{transform:scale(.96)} +.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);animation:dbIn .2s var(--ease)} +.kh-n{font-size:12px;color:var(--ink-3);padding:0 6px;font-variant-numeric:tabular-nums;transition:transform .2s var(--spring)} +.kh-btn{width:26px;height:26px;opacity:0;border-radius:6px;transition:opacity .15s,background .15s,transform .2s var(--spring),color .15s} +.kh-btn .i{width:14px;height:14px;transition:transform .25s var(--spring)} +.kh-btn:hover{background:var(--hover);color:var(--ink);transform:scale(1.08)} +.kh-btn:hover .i{transform:scale(1.15) rotate(-6deg)} +.kh-btn:active{transform:scale(.92)} .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} +.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;animation:dbIn .3s var(--ease) backwards;transition:box-shadow .2s var(--ease),transform .25s var(--spring),opacity .15s,background .15s} +.kcard:hover{transform:translateY(-2px);box-shadow:0 0 0 1px var(--line-2),0 10px 22px -14px rgba(0,0,0,.55)} +.kcard:active{transform:scale(.985)} +.kcard.editing{cursor:text;box-shadow:0 0 0 1.5px var(--blush-line);transform:scale(1.01);background:var(--raise)} +.kcard.lifted{opacity:.45;transform:rotate(1.2deg) scale(.98)} +.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,transform .2s var(--spring)} +.kcheck:hover{transform:scale(1.12);box-shadow:inset 0 0 0 1.5px var(--blush)} +.kcheck:active{transform:scale(.88)} +.kcheck.on{background:var(--blush);box-shadow:inset 0 0 0 1.5px var(--blush);animation:dbTick .3s var(--spring)} .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)} +.kcard.done .kc-text{color:var(--ink-3);text-decoration:line-through;text-decoration-color:var(--ink-3);transition:color .2s} .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} +.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;animation:dbIn .2s var(--ease)} +.kmore{position:absolute;top:4px;right:4px;width:24px;height:24px;background:var(--raise);opacity:0;border-radius:5px;transition:opacity .15s,background .15s,transform .2s var(--spring),color .15s} +.kmore .i{width:13px;height:13px;transition:transform .2s var(--spring)} +.kmore:hover{background:var(--hover);color:var(--ink);transform:scale(1.08)} +.kmore:hover .i{transform:scale(1.12)} +.kmore:active{transform:scale(.92)} .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} +.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,transform .2s var(--spring)} +.kadd:hover{background:var(--hover);color:var(--ink);transform:translateY(-1px)} +.kadd:active{transform:scale(.97)} +.kadd .i{width:14px;height:14px;transition:transform .25s var(--spring),color .15s} +.kadd:hover .i{transform:rotate(90deg) scale(1.15);color:var(--blush)} +.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,box-shadow .2s var(--ease),transform .25s var(--spring)} +.kaddcol:hover{background:var(--hover);color:var(--ink);box-shadow:inset 0 0 0 1.5px var(--blush-line);transform:translateY(-2px)} +.kaddcol:active{transform:scale(.98)} +.kaddcol .i{transition:transform .3s var(--spring),color .15s} +.kaddcol:hover .i{transform:rotate(90deg) scale(1.2);color:var(--blush)} +.kdrop{position:absolute;left:4px;right:4px;height:3px;border-radius:3px;background:var(--blush);pointer-events:none;transition:top .1s var(--ease),opacity .15s;animation:kdropIn .18s var(--ease)} +.kdrop-col{position:absolute;top:0;bottom:0;width:3px;border-radius:3px;background:var(--blush);pointer-events:none;transition:left .1s var(--ease),opacity .15s;animation:kdropIn .18s var(--ease)} +@keyframes kdropIn{from{opacity:0;transform:scaleX(.8)}} /* The floating copy is a clone of a card or column, which carry hover and move transitions: none of those may apply, or it would slide in from the corner and trail behind the pointer. */ .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;transition:none!important;animation:none!important;will-change:transform} @@ -553,7 +699,16 @@ body.page-dragging,body.page-dragging *{cursor:grabbing!important;user-select:no .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} +.panel.settings{width:min(720px,100%);height:min(560px,calc(100vh - 12vh - 32px));display:flex;flex-direction:column} +.set-main{display:flex;flex:1;min-height:0} +.set-nav{display:flex;flex-direction:column;gap:2px;width:172px;flex:none;padding:10px;border-right:1px solid var(--line)} +.set-nav button{display:flex;align-items:center;gap:9px;height:32px;padding:0 10px;border-radius:6px;color:var(--ink-2);font-weight:550;text-align:left} +.set-nav button .i{width:15px;height:15px;flex:none} +.set-nav button:hover{background:var(--hover);color:var(--ink)} +.set-nav button.on{background:var(--hover);color:var(--ink);box-shadow:inset 2px 0 0 var(--blush)} +.set-main .set-body{flex:1;min-width:0} +.set-body h3:first-child{margin-top:12px} +.reduce-motion *,.reduce-motion *::before,.reduce-motion *::after{animation-duration:.001s!important;animation-delay:0s!important;transition-duration:.001s!important;scroll-behavior:auto!important} .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} @@ -578,6 +733,36 @@ body.page-dragging,body.page-dragging *{cursor:grabbing!important;user-select:no .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} +.fp{display:flex;align-items:center;gap:7px;width:230px;height:32px;padding:0 4px 0 10px;border-radius:7px;background:var(--hover);color:var(--ink-3);box-shadow:inset 0 0 0 1px var(--line);transition:box-shadow .15s} +.fp:focus-within{box-shadow:inset 0 0 0 1.5px var(--blush-line)} +.fp>.i{width:14px;height:14px;flex:none} +.fp.sm{width:230px;padding:0 4px 0 12px;text-align:left;cursor:pointer;color:var(--ink)} +.fp.sm:hover{background:var(--press)} +.fp.sm.open{box-shadow:inset 0 0 0 1.5px var(--blush-line)} +.sm-val{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px} +.sm-val.muted{color:var(--ink-3)} +.fp.sm .fp-chev{pointer-events:none} +.pop.fp-list.sm-list{display:flex;flex-direction:column;padding:0;overflow:hidden} +.sm-find{display:flex;align-items:center;gap:8px;height:38px;padding:0 12px;flex:none;border-bottom:1px solid var(--line);color:var(--ink-3)} +.sm-find .i{width:14px;height:14px;flex:none} +.sm-find input{flex:1;min-width:0;height:100%;border:0;outline:0;background:none;color:var(--ink);font:13px var(--f-ui)} +.sm-rows{flex:1;min-height:0;overflow-y:auto;padding:5px;overscroll-behavior:contain} +.fp-input{flex:1;min-width:0;height:100%;border:0;outline:0;background:none;color:var(--ink);font:13px var(--f-ui);text-overflow:ellipsis} +.fp-input::placeholder{color:var(--ink-3)} +.fp-chev{width:24px;height:24px;display:grid;place-items:center;border-radius:5px;color:var(--ink-3);flex:none} +.fp-chev:hover{background:var(--press);color:var(--ink)} +.fp-chev .i{width:13px;height:13px;transform:rotate(90deg)} +.pop.fp-list.up{transform-origin:bottom right} +.pop.fp-list{position:fixed;z-index:90;overflow-y:auto;padding:5px;border-radius:10px;animation:pop .16s var(--spring);transform-origin:top right} +.fp-list .mi{height:34px;padding:0 9px} +.fp-list .mi.act{background:var(--hover);color:var(--ink)} +.fp-list .mi-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.fp-list mark{background:none;color:var(--blush);font-weight:600} +.fp-list em{font-style:normal;font-size:11.5px;color:var(--ink-3);max-width:46%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.fp-list .mi>.i:last-child{width:14px;height:14px;color:var(--blush);flex:none} +.fp-none{margin:0;padding:10px;color:var(--ink-3);font-size:12.5px} +.set-link{color:var(--blush);text-decoration:underline;text-underline-offset:3px} +.set-note b{color:var(--ink-2);font-weight:550} .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)} @@ -601,7 +786,7 @@ body.page-dragging,body.page-dragging *{cursor:grabbing!important;user-select:no .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{position:absolute;left:0;top:0;height:3px;border-radius:3px;background:var(--blush);opacity:0;pointer-events:none;transition:opacity .12s} .drop.show{opacity:1} .tail{height:32vh;cursor:text} @@ -618,6 +803,7 @@ body.page-dragging,body.page-dragging *{cursor:grabbing!important;user-select:no .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} +.si kbd{margin-left:auto} .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)} @@ -709,6 +895,9 @@ body.page-dragging,body.page-dragging *{cursor:grabbing!important;user-select:no /* ---------- desktop shell ---------- */ .topbar,.ws{-webkit-app-region:drag} .topbar button,.topbar .menu,.ws button{-webkit-app-region:no-drag} +/* the whole sidebar drags the window; anything clickable, draggable or scrollable inside it does not */ +.sidebar{-webkit-app-region:drag} +.sidebar :is(button,input,textarea,a,select,.row,.tree,.favs,.tags,.tag,.qa,[tabindex],[draggable=true]){-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} @@ -717,8 +906,6 @@ body.page-dragging,body.page-dragging *{cursor:grabbing!important;user-select:no .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} @@ -786,3 +973,722 @@ body.page-dragging,body.page-dragging *{cursor:grabbing!important;user-select:no overflow: hidden; text-overflow: ellipsis; } + +/* canvas (CanvasView.svelte): world coordinates scale with the zoom; --iz undoes it for handles */ +.cv{position:relative;flex:1;min-height:0;overflow:hidden;outline:none;cursor:default;user-select:none;touch-action:none; + border-top:1px solid var(--line);border-radius:0 0 12px 12px; + background-color:var(--island);background-image:radial-gradient(circle,var(--line-2) 1px,transparent 1.3px); + background-size:var(--gs) var(--gs);background-position:var(--gx) var(--gy)} +.cv.panning,.cv.panning *{cursor:grab!important} +.cv-world{position:absolute;left:0;top:0;width:0;height:0;transform-origin:0 0} +.cv-node{position:absolute;box-sizing:border-box;--cc:var(--line-2)} +.cv-card{display:flex;flex-direction:column;border-radius:10px;background:var(--raise);box-shadow:0 0 0 1.5px var(--line-2),0 2px 10px -4px rgba(0,0,0,.35);cursor:default} +.cv-card.colored{background:color-mix(in srgb,var(--cc) 10%,var(--raise));box-shadow:0 0 0 1.5px var(--cc),0 2px 10px -4px rgba(0,0,0,.35)} +.cv-card.sel,.cv-card.editing{box-shadow:0 0 0 calc(2px * var(--iz)) var(--blush),0 0 0 calc(5px * var(--iz)) var(--blush-soft)} +.cv-card.target,.cv-group.target{box-shadow:0 0 0 calc(2.5px * var(--iz)) var(--blush)} +.cv-group{border-radius:14px;background:color-mix(in srgb,var(--cc) 6%,transparent);box-shadow:inset 0 0 0 1.5px var(--line-2)} +.cv-group.colored{background:color-mix(in srgb,var(--cc) 9%,transparent);box-shadow:inset 0 0 0 1.5px color-mix(in srgb,var(--cc) 70%,transparent)} +.cv-group.sel{box-shadow:inset 0 0 0 calc(2px * var(--iz)) var(--blush)} +.cv-gtoggle{position:absolute;left:8px;top:8px;width:26px;height:26px;display:grid;place-items:center;border-radius:6px;color:var(--ink-3);opacity:0;z-index:2;transition:opacity .12s,background .12s;transform:scale(var(--iz));transform-origin:0 0} +.cv-gtoggle .i{width:15px;height:15px;transition:transform .2s var(--ease);transform:rotate(90deg)} +.cv-group.collapsed .cv-gtoggle .i{transform:none} +.cv-group:hover>.cv-gtoggle,.cv-group.sel>.cv-gtoggle,.cv-group.collapsed>.cv-gtoggle{opacity:.85} +.cv-gtoggle:hover{background:var(--hover);color:var(--ink);opacity:1} +.cv-group.collapsed{background:color-mix(in srgb,var(--cc) 14%,var(--raise));box-shadow:0 0 0 1.5px var(--line-2),0 2px 10px -4px rgba(0,0,0,.35)} +.cv-group.collapsed.colored{box-shadow:0 0 0 1.5px var(--cc),0 2px 10px -4px rgba(0,0,0,.35)} +.cv-group.collapsed.sel{box-shadow:0 0 0 calc(2px * var(--iz)) var(--blush),0 0 0 calc(5px * var(--iz)) var(--blush-soft)} +.cv-gcount{position:absolute;left:42px;top:0;bottom:0;display:flex;align-items:center;font-size:13.5px;color:var(--ink-3);pointer-events:none} +.cv-glabel{position:absolute;left:0;bottom:100%;margin-bottom:6px;max-width:100%;padding:3px 10px;border-radius:7px;font:600 15px/1.4 var(--f-ui);color:var(--ink);background:var(--raise);box-shadow:0 0 0 1px var(--line-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.cv-group.colored .cv-glabel{background:color-mix(in srgb,var(--cc) 22%,var(--raise));box-shadow:0 0 0 1px var(--cc)} +.cv-glabel.empty{color:var(--ink-3)} +input.cv-glabel{min-width:160px;outline:none;box-shadow:0 0 0 2px var(--blush);user-select:text} +.cv-scroll{flex:1;min-height:0;overflow:auto;overscroll-behavior:contain} +.cv-md{padding:10px 14px;font-size:15px;line-height:1.55;color:var(--ink);word-break:break-word} +.cv-md>.eb:first-child{margin-top:0} +.cv-md .eb.b-h1 .c{font-size:1.5em}.cv-md .eb.b-h2 .c{font-size:1.28em}.cv-md .eb.b-h3 .c{font-size:1.12em} +.cv-md .wl,.cv-md a{cursor:pointer} +.cv-muted{color:var(--ink-3);font-size:13.5px} +.cv-desc{margin:0 0 8px;color:var(--ink-2);font-size:14px;line-height:1.5} +.cv-url{color:var(--ink-2);font:13px/1.5 var(--f-mono);word-break:break-all} +.cv-ed{--fs:15px;padding:6px 14px 10px 14px;font-size:15px;line-height:1.55;color:var(--ink);cursor:text;user-select:text} +.cv-ed .blocks{padding-top:0} +.cv-ed .gutter,.cv-ed .tail{display:none} +.cv-ed .b-h1{margin-top:8px}.cv-ed .b-h2{margin-top:6px}.cv-ed .b-h3{margin-top:4px} +.cv-ed .b-h1 .c{font-size:1.5em}.cv-ed .b-h2 .c{font-size:1.28em}.cv-ed .b-h3 .c{font-size:1.12em} +.cv-fhead{display:flex;align-items:center;gap:8px;height:38px;padding:0 12px;flex:none;border-bottom:1px solid var(--line);color:var(--ink-3)} +.cv-card.colored .cv-fhead{border-bottom-color:color-mix(in srgb,var(--cc) 40%,transparent)} +.cv-fhead .i{width:15px;height:15px;flex:none} +.cv-fhead .i.emo{font-size:14px} +.cv-ftitle{min-width:0;font-weight:600;font-size:14px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:left} +.cv-ftitle:hover{color:var(--blush);text-decoration:underline;text-underline-offset:3px} +.cv-image{background:none;box-shadow:none;overflow:visible} +.cv-image img{width:100%;height:100%;object-fit:contain;border-radius:8px;pointer-events:none} +.cv-image.sel img{outline:calc(2px * var(--iz)) solid var(--blush);outline-offset:calc(2px * var(--iz))} +/* connection dots on the four sides, and the resize corner (shown on hover / selection, constant size on screen) */ +.cv-dot{position:absolute;width:14px;height:14px;margin:-7px 0 0 -7px;border-radius:50%;background:var(--raise);box-shadow:0 0 0 2px var(--blush);transform:scale(var(--iz));opacity:0;transition:opacity .12s;cursor:crosshair;z-index:2} +.cv-dot[data-side=top]{left:50%;top:0}.cv-dot[data-side=bottom]{left:50%;top:100%}.cv-dot[data-side=left]{left:0;top:50%}.cv-dot[data-side=right]{left:100%;top:50%} +.cv-dot:hover{background:var(--blush)} +.cv-resize{position:absolute;right:0;bottom:0;width:16px;height:16px;transform:scale(var(--iz));transform-origin:100% 100%;cursor:nwse-resize;opacity:0;z-index:2; + background:linear-gradient(135deg,transparent 55%,var(--blush) 55%,var(--blush) 65%,transparent 65%,transparent 75%,var(--blush) 75%,var(--blush) 85%,transparent 85%)} +.cv-node:hover>.cv-dot,.cv-node.sel>.cv-dot,.cv-node:hover>.cv-resize,.cv-node.sel>.cv-resize,.cv.linking .cv-node>.cv-dot{opacity:1} +.cv-node.editing>.cv-dot,.cv-node.editing>.cv-resize,.cv.panning .cv-dot{opacity:0!important} +/* arrows */ +.cv-edges{position:absolute;left:0;top:0;overflow:visible;pointer-events:none} +.cv-edge{--ec:var(--ink-3)} +.cv-line{fill:none;stroke:var(--ec);stroke-width:2.5;stroke-linecap:round} +.cv-arrow{fill:var(--ec)} +.cv-hit{fill:none;stroke:transparent;stroke-width:16;pointer-events:stroke;cursor:pointer} +.cv-edge:hover .cv-line{stroke-width:3.5} +.cv-edge.sel{--ec:var(--blush)} +.cv-edge.sel .cv-line{stroke-width:3.5} +.cv-edge.drawing{--ec:var(--blush)} +.cv-edge.drawing .cv-line{stroke-dasharray:7 6} +.cv-elabel{position:absolute;transform:translate(-50%,-50%);max-width:240px;padding:3px 10px;border-radius:7px;background:var(--island);box-shadow:0 0 0 1px var(--line-2);font-size:13.5px;color:var(--ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer} +.cv-elabel.sel{color:var(--ink);box-shadow:0 0 0 2px var(--blush)} +input.cv-elabel{min-width:120px;text-align:center;outline:none;box-shadow:0 0 0 2px var(--blush);color:var(--ink);user-select:text;cursor:text} +.cv-guide{position:absolute;background:var(--blush);opacity:.85;pointer-events:none;z-index:3} +.cv-box{position:absolute;border:calc(1px * var(--iz)) solid var(--blush);background:var(--blush-soft);border-radius:4px;pointer-events:none} +/* floating controls */ +.cv.dropping::after{content:'';position:absolute;inset:0;z-index:9;pointer-events:none;box-shadow:inset 0 0 0 2px var(--blush);background:var(--blush-soft)} +.cv-ui.cv-find{top:12px;left:50%;translate:-50% 0;gap:4px;padding:4px 6px 4px 12px;color:var(--ink-3);animation:pop .22s var(--spring)} /* `translate`, so the pop animation's transform doesn't undo the centring */ +.cv-ui.cv-find input{width:210px;height:30px;background:none;border:0;outline:none;color:var(--ink);font-size:14px;user-select:text} +.cv-count{min-width:62px;text-align:right;font:12px var(--f-mono);color:var(--ink-3)} +.cv-count.none{color:var(--danger)} +.cv-prev .i{transform:rotate(-90deg)}.cv-next .i{transform:rotate(90deg)} +.cv.finding .cv-node:not(.found),.cv.finding .cv-edge:not(.found),.cv.finding .cv-elabel:not(.found){opacity:.3} +.cv-node.found{box-shadow:0 0 0 calc(2px * var(--iz)) var(--blush-line),0 2px 10px -4px rgba(0,0,0,.35)} +.cv-node.found-cur{box-shadow:0 0 0 calc(3px * var(--iz)) var(--blush),0 0 0 calc(8px * var(--iz)) var(--blush-soft)} +.cv-elabel.found{color:var(--ink);box-shadow:0 0 0 2px var(--blush-line)} +.cv-elabel.found-cur{box-shadow:0 0 0 2px var(--blush),0 0 0 5px var(--blush-soft)} +.cv-edge.found{--ec:var(--blush)} +.cv-ui{position:absolute;z-index:5;display:flex;align-items:center;gap:2px;padding:4px;border-radius:10px;background:var(--raise);box-shadow:0 0 0 1px var(--line-2),0 8px 24px -10px rgba(0,0,0,.5)} +.cv-bar{left:50%;bottom:16px;transform:translateX(-50%)} +.cv-zoom{right:14px;bottom:16px} +.cv-pct{min-width:48px;height:30px;border-radius:6px;font:500 12px var(--f-mono);color:var(--ink-2)} +.cv-pct:hover{background:var(--hover);color:var(--ink)} +.cv-selbar{transform:translateX(-50%);gap:4px;padding:5px 6px} +.cv-swatch{width:20px;height:20px;border-radius:50%;background:var(--sw,transparent);box-shadow:inset 0 0 0 1.5px var(--line-2);transition:transform .15s var(--spring)} +.cv-swatch:not([style]){background:linear-gradient(135deg,transparent 45%,var(--danger) 45%,var(--danger) 55%,transparent 55%)} +.cv-swatch:hover{transform:scale(1.15)} +.cv-swatch.on{box-shadow:0 0 0 2px var(--raise),0 0 0 3.5px var(--ink-2)} +.cv-menu{flex-direction:column;align-items:stretch;gap:0;width:216px;padding:6px;z-index:8} +.cv-menu .mi{height:32px} +.mi-tick{display:inline-grid;place-items:center;width:15px;height:15px;flex:none} +.mi-tick .i{width:14px;height:14px} +.cv-mswatches{display:flex;justify-content:space-between;padding:6px 8px} +.cv-sep{width:1px;height:20px;margin:0 3px;background:var(--line-2)} +.cv-picker{position:absolute;left:50%;bottom:calc(100% + 8px);transform:translateX(-50%);width:300px;padding:6px} +.cv-picker input{width:100%;height:34px;padding:0 10px;border-radius:7px;background:var(--hover);color:var(--ink);outline:none;margin-bottom:4px} +.cv-plist{max-height:290px;overflow:auto} +.cv-plist .mi.act{background:var(--hover);color:var(--ink)} +.cv-plist .mi-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.cv-plist em{font-style:normal;font-size:11.5px;color:var(--ink-3);max-width:40%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.cv-none{padding:8px 10px;margin:0} +.cv-empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;color:var(--ink-3);text-align:center;pointer-events:none;font-size:14px;line-height:1.6} +.cv-empty .i{width:34px;height:34px;opacity:.6} +.cv-empty p{margin:0} +.cv-broken{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;color:var(--ink-2);text-align:center;padding:24px} +.cv-broken .i{width:34px;height:34px;color:var(--ink-3)} +.cv-broken p{margin:0} +.cv-why{font:12.5px var(--f-mono);color:var(--danger)} + +/* database: a page of records shown as a table, board, calendar, gallery, timeline or list (Database.svelte) */ +.i.flip{transform:scaleX(-1)} +.db{padding-top:6px;padding-bottom:40px} +.db-bar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:4px 0 10px;border-bottom:1px solid var(--line);margin-bottom:10px} +.db-tabs{display:flex;align-items:center;gap:2px;flex-wrap:wrap;min-width:0} +.db-tab{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px;border-radius:6px;color:var(--ink-3);font-size:13px;transition:background .15s,color .15s} +.db-tab:hover{background:var(--hover);color:var(--ink)} +.db-tab.on{color:var(--ink);background:var(--press)} +.db-tab .i{width:14px;height:14px} +.db-tab.add{padding:0 8px} +.db-tools{display:flex;align-items:center;gap:4px;margin-left:auto;flex-wrap:wrap} +.db-tool{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px;border-radius:6px;color:var(--ink-3);font-size:13px;transition:background .15s,color .15s} +.db-tool:hover{background:var(--hover);color:var(--ink)} +.db-tool.on{color:var(--blush)} +.db-tool .i{width:14px;height:14px} +.db-new{height:30px;padding:0 12px;margin-left:6px} +.db-scroll{overflow:auto;max-width:100%;padding-bottom:8px} +.db-hint{color:var(--ink-3);font-size:13px;padding:14px 4px;display:flex;align-items:center;gap:8px;flex-wrap:wrap} +.db-none{color:var(--ink-3);font-size:12.5px;padding:8px 10px} +/* values */ +.db-opt{display:inline-flex;align-items:center;height:22px;padding:0 9px;border-radius:99px;font-size:12.5px;color:var(--ink);white-space:nowrap;background:color-mix(in srgb,var(--oc) 20%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--oc) 34%,transparent)} +.db-dot{width:10px;height:10px;border-radius:50%;background:var(--oc);flex:none} +.db-person{display:inline-flex;align-items:center;gap:6px;height:22px;padding:0 4px 0 3px;border-radius:99px;background:var(--hover);font-size:12.5px;color:var(--ink);white-space:nowrap} +.db-person i{font-style:normal;width:16px;height:16px;border-radius:50%;display:grid;place-items:center;background:var(--blush-soft);color:var(--blush);font-size:10px;font-weight:600} +.db-person:not(:has(.chip-x)){padding-right:9px} +.db-file{display:inline-flex;align-items:center;max-width:180px;height:22px;border-radius:6px;background:var(--hover);font-size:12.5px} +.db-file-name{display:inline-flex;align-items:center;gap:5px;padding:0 7px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink-2)} +.db-file-name .i{width:12px;height:12px} +button.db-file-name:hover{color:var(--ink)} +.db-txt{font-size:13px;color:var(--ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.db-ck,.dbx-box{width:16px;height:16px;border-radius:4px;display:grid;place-items:center;box-shadow:inset 0 0 0 1.5px var(--line-2);flex:none;color:transparent} +.db-ck .i,.dbx-box .i{width:12px;height:12px;stroke-width:2.6} +.db-ck.on,.dbx.on .dbx-box{background:var(--blush);box-shadow:none;color:var(--blush-ink)} +.dbx{display:inline-flex;cursor:pointer;padding:6px 0} +.dbx input{position:absolute;opacity:0;pointer-events:none} +.dbx input:focus-visible+.dbx-box{outline:2px solid var(--blush-line);outline-offset:2px} +.db-chips{display:flex;align-items:center;gap:4px;flex-wrap:wrap;min-height:30px} +.db-add{width:110px} +.db-plus{opacity:.55} +.db-chips:hover .db-plus,.db-plus:focus-visible{opacity:1} +.db-in{width:100%;height:30px;border:0;outline:0;background:transparent;color:var(--ink);font:13px var(--f-ui);padding:0 6px;border-radius:5px;min-width:0} +.db-in:hover{background:var(--hover)} +.db-in:focus{background:var(--hover);box-shadow:inset 0 0 0 1px var(--blush-line)} +.db-in.num{text-align:right;font-variant-numeric:tabular-nums} +.db-in::-webkit-calendar-picker-indicator{opacity:.5;cursor:pointer} +:root:not([data-theme="light"]) .db-in::-webkit-calendar-picker-indicator{filter:invert(.8)} +.db-sel{display:flex;align-items:center;gap:4px;flex-wrap:wrap;width:100%;min-height:30px;padding:3px 6px;border-radius:5px;text-align:left} +.db-sel:hover{background:var(--hover)} +.db-empty{color:var(--ink-3);font-size:13px} +.db-pv{overflow:visible;white-space:normal} +.db-pv .db-in{height:28px} +.db-pop{display:flex;flex-direction:column;gap:4px;z-index:90;overflow:hidden} +.db-list{overflow:auto;display:flex;flex-direction:column} +.db-find{width:100%;height:30px;border:0;outline:0;border-radius:6px;padding:0 9px;margin-bottom:4px;background:var(--hover);color:var(--ink);font:13px var(--f-ui)} +.db-find:focus{box-shadow:inset 0 0 0 1px var(--blush-line)} +.db-pop .mi{height:30px} +.db-pop .mi .mi-tick{margin-left:auto} +/* table */ +.dbt{border-collapse:separate;border-spacing:0;table-layout:fixed;font-size:13px} +.dbt th{position:sticky;top:0;background:var(--island);z-index:2;text-align:left;font-weight:400;border-bottom:1px solid var(--line);padding:0} +.dbt td{border-bottom:1px solid var(--line);padding:0 2px;vertical-align:middle;overflow:hidden} +.dbt th.db-name,.dbt td.db-name{position:sticky;left:34px;z-index:1;background:var(--island)} +.dbt th.db-selcol,.dbt td.db-selcol{position:sticky;left:0;z-index:3;background:var(--island);padding:0;text-align:center} +.dbt th.db-selcol{z-index:4} +.db-selcol .dbx{padding:0;width:34px;height:34px;align-items:center;justify-content:center} +tbody .db-selcol .dbx{opacity:0;transition:opacity .12s} +tr:hover .db-selcol .dbx,tr.picked .db-selcol .dbx,.db-selcol .dbx:focus-within,.dbt:has(tr.picked) .db-selcol .dbx{opacity:1} +.dbt tr.picked td{background:var(--blush-soft)} +.dbt tr.picked td.db-name,.dbt tr.picked td.db-selcol{background:color-mix(in srgb,var(--island) 80%,var(--blush))} +.dbt th.db-name{z-index:3} + +.dbt tbody tr[data-rec]:hover td{background:color-mix(in srgb,var(--hover) 100%,var(--island))} +.dbt tbody tr[data-rec]:hover td.db-name{background:color-mix(in srgb,var(--island) 94%,var(--ink))} +.db-th{display:flex;align-items:center;gap:7px;width:100%;height:34px;padding:0 8px;color:var(--ink-3);font-size:12.5px;white-space:nowrap;overflow:hidden;cursor:grab;border-radius:5px;transition:background .15s,color .15s} +.db-th:hover{background:var(--hover);color:var(--ink)} +.db-th .i{width:13px;height:13px} +.db-th.add{justify-content:center} +.db-rowhead{display:flex;align-items:center;gap:2px;height:36px} +.db-ico{display:grid;place-items:center;width:26px;height:26px;border-radius:5px;color:var(--ink-3);flex:none} +.db-ico:hover{background:var(--hover);color:var(--ink)} +.db-ico .i{width:15px;height:15px} +.db-title{flex:1;min-width:0;height:28px;border:0;outline:0;background:transparent;color:var(--ink);font:500 13.5px var(--f-ui);padding:0 6px;border-radius:5px} +.db-title:focus{background:var(--hover);box-shadow:inset 0 0 0 1px var(--blush-line)} +.db-row-x{display:grid;place-items:center;width:24px;height:24px;border-radius:5px;color:var(--ink-3);opacity:0;flex:none;transition:opacity .15s,background .15s} +.db-row-x .i{width:13px;height:13px} +.db-row-x:hover{background:var(--hover);color:var(--danger)} +tr:hover .db-row-x,.db-lrow:hover .db-row-x,.db-row-x:focus-visible{opacity:1} +.db-addrow td{border-bottom:0;padding:4px 6px} +/* board */ +.db-board{display:flex;gap:12px;align-items:flex-start;overflow-x:auto;padding:4px 2px 14px} +.db-col-title{color:var(--ink-3);font-size:12.5px} +.db-count{color:var(--ink-3);font-size:12px;font-variant-numeric:tabular-nums} +.db-card.lifted,.db-ev.lifted{opacity:.45} +.db-card-title{display:flex;align-items:center;gap:7px;font-weight:500;font-size:13.5px;color:var(--ink);min-width:0} +.db-card-title .i{width:14px;height:14px;color:var(--ink-3);flex:none} +.db-card-props{display:flex;flex-wrap:wrap;gap:4px;align-items:center;min-width:0} +.db-kv{display:inline-flex;flex-wrap:wrap;gap:4px;align-items:center;min-width:0} +/* calendar */ +.db-cal-head{display:flex;align-items:center;gap:8px;margin-bottom:8px} +.db-cal-head strong{min-width:150px;text-align:center;font-weight:600} +.db-cal-head .btn{margin-left:auto} +.db-cal-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));border-top:1px solid var(--line);border-left:1px solid var(--line);border-radius:8px;overflow:hidden} +.db-wd{padding:6px 8px;font-size:12px;color:var(--ink-3);border-right:1px solid var(--line);border-bottom:1px solid var(--line)} +.db-day{min-height:104px;padding:4px;display:flex;flex-direction:column;gap:3px;border-right:1px solid var(--line);border-bottom:1px solid var(--line);min-width:0;transition:background .15s} +.db-day.out{background:color-mix(in srgb,var(--ink) 2.5%,transparent)} +.db-day.out .db-day-head span{opacity:.45} +.db-day.over{background:var(--blush-soft)} +.db-day-head{display:flex;align-items:center;justify-content:space-between;font-size:12px;color:var(--ink-2);padding:0 2px} +.db-day.today .db-day-head span{background:var(--blush);color:var(--blush-ink);border-radius:99px;min-width:20px;height:20px;display:inline-grid;place-items:center;padding:0 5px;font-weight:600} +.db-day-add{width:20px;height:20px;display:grid;place-items:center;border-radius:5px;color:var(--ink-3);opacity:0} +.db-day-add .i{width:12px;height:12px} +.db-day:hover .db-day-add,.db-day-add:focus-visible{opacity:1} +.db-day-add:hover{background:var(--hover);color:var(--ink)} +.db-ev{display:block;width:100%;text-align:left;height:22px;line-height:22px;padding:0 7px;border-radius:5px;background:var(--blush-soft);color:var(--ink);font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;box-shadow:inset 2px 0 0 var(--blush)} +.db-ev:hover{background:color-mix(in srgb,var(--blush) 22%,transparent)} +.db-more{font-size:11.5px;color:var(--ink-3);padding:0 6px} +.db-ev.static{display:inline-flex;align-items:center;width:auto;gap:2px;padding:0 2px 0 7px} +.db-ev.static button:first-child{max-width:200px;overflow:hidden;text-overflow:ellipsis} +/* gallery */ +.db-gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;padding:4px 0} +.db-gcard{display:flex;flex-direction:column;border-radius:10px;overflow:hidden;background:var(--island-2);box-shadow:0 0 0 1px var(--line);cursor:pointer;text-align:left;transition:box-shadow .15s,transform .2s var(--spring)} +.db-gcard:hover{box-shadow:0 0 0 1px var(--line-2);transform:translateY(-1px)} +.db-gcover{height:120px;display:grid;place-items:center;background:linear-gradient(135deg,var(--c1),var(--c2));color:rgba(255,255,255,.75)} +.db-gcover .i{width:28px;height:28px} +.db-gcover img{width:100%;height:100%;object-fit:cover} +.db-gbody{display:flex;flex-direction:column;gap:6px;padding:10px 12px 12px} +.db-gnew{min-height:80px;align-items:center;justify-content:center;flex-direction:row;gap:6px;color:var(--ink-3);background:transparent;box-shadow:0 0 0 1px var(--line);border-style:dashed} +/* timeline */ +.db-tl-tools{display:flex;gap:2px;justify-content:flex-end;margin-bottom:4px} +.db-tl{position:relative;width:calc(var(--span) * var(--px));min-width:100%} +.db-tl-months{display:flex;border-bottom:1px solid var(--line);font-size:12px;color:var(--ink-2);height:24px} +.db-tl-months span{padding:3px 6px;border-right:1px solid var(--line);overflow:hidden;white-space:nowrap;flex:none} +.db-tl-days{display:flex;height:22px;font-size:11px;color:var(--ink-3);border-bottom:1px solid var(--line)} +.db-tl-days span{flex:0 0 var(--px);text-align:center;line-height:22px;overflow:visible;white-space:nowrap} +.db-tl-days span.wk{background:color-mix(in srgb,var(--ink) 3%,transparent)} +.db-tl-days span.today{color:var(--blush);font-weight:600} +.db-tl-rows{position:relative;padding:6px 0;background-image:repeating-linear-gradient(to right,transparent 0,transparent calc(var(--px) * 7 - 1px),var(--line) calc(var(--px) * 7 - 1px),var(--line) calc(var(--px) * 7))} +.db-tl-today{position:absolute;top:0;bottom:0;width:2px;margin-left:-1px;background:var(--blush);opacity:.6;pointer-events:none} +.db-tl-row{height:30px;position:relative} +.db-tl-bar{position:absolute;top:3px;height:24px;border-radius:6px;background:var(--blush-soft);box-shadow:inset 0 0 0 1px var(--blush-line);color:var(--ink);font-size:12.5px;text-align:left;padding:0 8px;overflow:hidden;white-space:nowrap;cursor:grab;touch-action:none;min-width:6px} +.db-tl-bar:hover{background:color-mix(in srgb,var(--blush) 24%,transparent)} +.db-tl-bar.moving{cursor:grabbing;box-shadow:inset 0 0 0 1.5px var(--blush)} +.db-undated{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:12px 0 4px;border-top:1px solid var(--line);margin-top:8px} +.db-undated-label{color:var(--ink-3);font-size:12.5px;margin-right:4px} +/* list */ +.db-list{display:flex;flex-direction:column;padding:2px 0} +.db-lrow{display:flex;align-items:center;gap:10px;min-height:38px;padding:4px 8px;border-radius:6px;cursor:pointer;border-bottom:1px solid var(--line)} +.db-lrow:hover{background:var(--hover)} +.db-lrow>.i{color:var(--ink-3);width:15px;height:15px} +.db-lname{font-weight:500;font-size:13.5px;flex:none;max-width:40%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.db-lrow .db-card-props{flex:1} +.db-list>.chip-add{align-self:flex-start;margin:6px 0 0} +/* popups */ +.db-menu{width:300px;max-height:min(560px,calc(100vh - 80px));overflow:auto;z-index:80;transform-origin:top left} +.db-menu .db-find{margin:2px 0 6px} +.db-sub{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);padding:8px 8px 4px} +.db-sortrow{display:flex;align-items:center;gap:6px;padding:4px 8px;font-size:13px;color:var(--ink-2)} +.db-sortrow span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis} +.db-chip{height:24px;padding:0 8px;border-radius:99px;font-size:12px;color:var(--ink-3);box-shadow:inset 0 0 0 1px var(--line)} +.db-chip:hover{color:var(--ink)} +.db-chip.on{color:var(--blush);box-shadow:inset 0 0 0 1px var(--blush-line);background:var(--blush-soft)} +.db-filter{display:flex;flex-direction:column;gap:4px;padding:6px 8px;border-bottom:1px solid var(--line)} +.db-filter .db-in{box-shadow:inset 0 0 0 1px var(--line);height:32px} +.db-optrow{display:flex;align-items:center;gap:6px;padding:0 4px} +.db-optrow .db-in{height:28px} +.db-menu .mi:disabled{opacity:.4;pointer-events:none} +.db-tl-label{position:absolute;top:3px;line-height:24px;font-size:12.5px;color:var(--ink-2);white-space:nowrap;pointer-events:none} +.db-pv .db-in.num{text-align:left} + +/* database: column resize and order, bulk bar, option colours, number formats, calendar week + spans, timeline edges + arrows */ +.db-resize{position:absolute;top:0;right:-3px;width:7px;height:100%;cursor:col-resize;z-index:3;touch-action:none} +.db-resize::after{content:"";position:absolute;top:6px;bottom:6px;left:3px;width:1px;background:transparent;transition:background .15s} +.db-resize:hover::after{background:var(--blush)} +.dbt th.dragging{opacity:.45} +.dbt th.dropping{box-shadow:inset 2px 0 0 var(--blush)} +.db-bulk{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:6px 8px;margin-bottom:8px;border-radius:9px;background:var(--blush-soft);box-shadow:0 0 0 1px var(--blush-line);font-size:13px} +.db-bulk strong{font-weight:600;margin-right:4px} +.db-bulk .btn{height:30px} +.db-bulk-val{min-width:140px;max-width:260px;background:var(--island);border-radius:6px} +.db-seg{display:inline-flex;margin-left:auto;padding:2px;border-radius:7px;background:var(--hover)} +.db-seg button{height:26px;padding:0 10px;border-radius:5px;font-size:12.5px;color:var(--ink-3)} +.db-seg button.on{background:var(--raise);color:var(--ink);box-shadow:0 0 0 1px var(--line)} +.db-seg+.btn{margin-left:0} +.db-dotbtn{display:grid;place-items:center;width:26px;height:26px;border-radius:5px;flex:none} +.db-dotbtn:hover{background:var(--hover)} +.db-swatches{display:flex;gap:6px;flex-wrap:wrap;padding:4px 8px 8px 34px} +.db-swatches .cv-swatch{width:18px;height:18px} +.db-swatches .cv-swatch.on{box-shadow:0 0 0 2px var(--raise),0 0 0 3.5px var(--ink-2)} +.db-numfmt{display:flex;align-items:center;gap:8px;width:100%;height:30px;padding:0 6px;border-radius:5px;text-align:right;justify-content:flex-end;font-size:13px;color:var(--ink);font-variant-numeric:tabular-nums} +.db-numfmt:hover{background:var(--hover)} +.db-barline{position:relative;flex:1;min-width:40px;height:8px;border-radius:99px;background:var(--hover);overflow:hidden;display:inline-block} +.db-barline i{position:absolute;inset:0 auto 0 0;background:var(--blush);border-radius:99px;transition:width .2s var(--ease)} +.db-barnum{min-width:34px;font-size:12px;color:var(--ink-2);text-align:right} +.db-numfmt .db-barline{flex:1} +.db-kv .db-barline{flex:none;width:56px} +.db-cal-grid.week .db-day{min-height:340px} +.db-cal-grid.week .db-day-add{opacity:1} +.db-day.reach{background:var(--blush-soft)} +.db-evwrap{position:relative;display:block;margin:0 -4px} +.db-evwrap .db-ev{border-radius:5px} +.db-ev.cont{border-top-left-radius:0;border-bottom-left-radius:0;box-shadow:none;background:color-mix(in srgb,var(--blush) 16%,transparent)} +.db-ev.more{border-top-right-radius:0;border-bottom-right-radius:0} +.db-evwrap:has(.db-ev.cont) .db-ev{margin-left:0} +.db-grip{position:absolute;top:0;right:0;width:8px;height:100%;cursor:ew-resize;border-radius:0 5px 5px 0;touch-action:none;opacity:0;background:var(--blush);transition:opacity .12s} +.db-evwrap:hover .db-grip,.db-grip:focus-visible{opacity:.55} +.db-tl-grip{position:absolute;top:0;width:7px;height:100%;cursor:ew-resize;touch-action:none;opacity:0;background:var(--blush);transition:opacity .12s} +.db-tl-grip.l{left:0;border-radius:6px 0 0 6px} +.db-tl-grip.r{right:0;border-radius:0 6px 6px 0} +.db-tl-bar:hover .db-tl-grip,.db-tl-bar.moving .db-tl-grip{opacity:.55} +.db-tl-text{pointer-events:none;margin:0 2px} +.db-tl-arrows{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none;color:var(--ink-3)} +.db-tl-arrows path{fill:none;stroke:currentColor;stroke-width:1.5} +.db-tl-arrows path.late{color:var(--danger);stroke:var(--danger)} +/* a database shown inside a page (![[Tasks#Board]]) */ +.emb.live .emb-body{padding:0 10px 10px 10px} +.emb.live .emb-more{display:none} +.db.embedded{padding-top:0;padding-bottom:0} +.db.embedded .db-bar{padding-top:2px} +.db.embedded .db-scroll{max-height:420px;overflow:auto} +.db.embedded .db-cal-grid.week .db-day{min-height:200px} +/* CSV import dialog */ +.panel.csv{width:min(760px,100%);max-height:calc(100vh - 12vh - 32px);display:flex;flex-direction:column} +.csv-body{overflow:auto;padding:12px 18px;min-height:120px} +.csv-lead{color:var(--ink-2);margin:2px 0 10px;font-size:13px} +.csv-name{display:flex;align-items:center;gap:10px;margin-bottom:12px;color:var(--ink-3);font-size:12.5px} +.csv-name .db-find{flex:1;margin:0} +.csv-cols{display:flex;flex-direction:column;gap:2px} +.csv-col{display:grid;grid-template-columns:minmax(0,1fr) 16px minmax(0,1.2fr);align-items:center;gap:10px;padding:7px 8px;border-radius:8px;border-bottom:1px solid var(--line)} +.csv-col.skip{opacity:.55} +.csv-src{display:flex;flex-direction:column;min-width:0} +.csv-src b{font-weight:550;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.csv-src small{color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.csv-col>.i{color:var(--ink-3)} +.csv-dst{display:flex;align-items:center;gap:6px;min-width:0} +.csv-dst .fp{flex:1;min-width:0} +.csv-pname{flex:1;box-shadow:inset 0 0 0 1px var(--line);height:32px} +.csv-type{display:inline-flex;align-items:center;gap:6px;color:var(--ink-3);font-size:12.5px;white-space:nowrap} +.csv-type .i{width:13px;height:13px} +.csv-actions{display:flex;gap:8px} +.csv-actions .btn:disabled{opacity:.5;pointer-events:none} +.csv-dst>.fp:first-child{flex:0 0 150px} + +/* database: motion. Everything eases with the app's own curves; reduced-motion is handled by the global rule. */ +@keyframes dbIn{from{opacity:0;transform:translateY(5px) scale(.985)}} +@keyframes dbSlide{from{opacity:0;transform:translateY(-6px)}} +@keyframes dbTick{0%{transform:scale(.6)}60%{transform:scale(1.18)}100%{transform:none}} +@keyframes dbDraw{from{stroke-dashoffset:1}to{stroke-dashoffset:0}} +@keyframes dbRing{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--blush) 45%,transparent)}100%{box-shadow:0 0 0 7px transparent}} + +/* views: tabs and tools */ +.db-tab{position:relative;transition:background .18s var(--ease),color .18s,transform .22s var(--spring)} +.db-tab::after{content:"";position:absolute;left:10px;right:10px;bottom:2px;height:2px;border-radius:2px;background:var(--blush);transform:scaleX(0);transform-origin:left;transition:transform .32s var(--ease)} +.db-tab.on::after{transform:scaleX(1)} +.db-tab:hover{transform:translateY(-1px)} +.db-tab:active{transform:scale(.95)} +.db-tab .i,.db-tool .i{transition:transform .25s var(--spring)} +.db-tab:hover .i,.db-tool:hover .i{transform:scale(1.15) rotate(-6deg)} +.db-tab.add:hover .i{transform:rotate(90deg) scale(1.1)} +.db-tool{transition:background .18s var(--ease),color .18s,transform .22s var(--spring)} +.db-tool:active{transform:scale(.95)} +.db-new{transition:transform .22s var(--spring),filter .15s,box-shadow .2s var(--ease)} +.db-new:hover{transform:translateY(-1px);box-shadow:0 6px 16px -8px var(--blush)} +.db-new:active{transform:scale(.95)} +.db-new .i{transition:transform .3s var(--spring)} +.db-new:hover .i{transform:rotate(90deg)} +.db>.db-scroll,.db>.db-board,.db>.db-gallery,.db>.db-cal,.db>.db-list,.db>.db-tl-tools{animation:dbSlide .28s var(--ease) backwards} + +/* table */ +.dbt td{transition:background .15s var(--ease)} +.dbt tbody tr{transition:background .15s} +.db-th{transition:background .15s var(--ease),color .15s} +.db-th:active{cursor:grabbing} +.db-resize::after{transition:background .15s,top .2s var(--ease),bottom .2s var(--ease),width .15s} +.db-resize:hover::after,.db-resize:active::after{top:0;bottom:0;width:2px;left:2px} +.dbt tbody tr[data-rec]{animation:dbIn .3s var(--ease) backwards} +.db-row-x{transform:translateX(4px) scale(.85);transition:opacity .15s,background .15s,color .15s,transform .22s var(--spring)} +tr:hover .db-row-x,.db-lrow:hover .db-row-x,.db-row-x:focus-visible{transform:none} +.db-ico{transition:background .15s,color .15s,transform .22s var(--spring)} +.db-ico:hover{transform:scale(1.12) rotate(-5deg)} +.db-title{transition:background .15s,box-shadow .2s var(--ease)} +.db-in{transition:background .15s,box-shadow .2s var(--ease)} +.dbx-box{transition:background .18s var(--ease),box-shadow .18s var(--ease),color .15s,transform .2s var(--spring)} +.dbx:hover .dbx-box{box-shadow:inset 0 0 0 1.5px var(--ink-3);transform:scale(1.08)} +.dbx.on .dbx-box,.db-ck.on{animation:dbTick .3s var(--spring)} +.dbx:active .dbx-box{transform:scale(.88)} +.db-bulk{animation:dbSlide .25s var(--ease) backwards} +.db-sel{transition:background .15s var(--ease)} +.db-plus,.chip-add{transition:background .15s,color .15s,opacity .15s,transform .2s var(--spring)} +.db-plus:hover{transform:scale(1.08)} +.db-numfmt{transition:background .15s} + +/* options, people, files */ +.db-opt,.db-person,.db-file{transition:transform .2s var(--spring),box-shadow .2s var(--ease),background .15s} +.db-sel:hover .db-opt,.db-card:hover .db-opt{transform:translateY(-1px)} +.db-opt{animation:chipIn .3s var(--spring) backwards} +.db-person{animation:chipIn .3s var(--spring) backwards} +.db-person .chip-x{transition:opacity .15s,background .15s,transform .2s var(--spring)} +.db-person .chip-x:hover{transform:rotate(90deg) scale(1.15)} +.db-pop{transform-origin:top left} +.db-pop .mi{transition:background .12s,color .12s,transform .18s var(--ease),padding-left .18s var(--ease)} +.db-pop .mi:hover,.db-menu .mi:hover{padding-left:13px} +.db-menu .mi{transition:background .12s,color .12s,padding-left .18s var(--ease)} +.db-menu .db-chip{transition:background .15s,color .15s,box-shadow .15s,transform .2s var(--spring)} +.db-menu .db-chip:hover{transform:translateY(-1px)} +.db-swatches .cv-swatch{transition:transform .2s var(--spring),box-shadow .2s var(--ease)} +.db-swatches .cv-swatch:hover{transform:scale(1.2)} +.db-dotbtn .db-dot{transition:transform .22s var(--spring)} +.db-dotbtn:hover .db-dot{transform:scale(1.35)} +.db-seg button{transition:background .18s var(--ease),color .15s,box-shadow .18s} +.db-seg button:active{transform:scale(.94)} + +/* board */ +.db-col{transition:box-shadow .2s var(--ease),background .2s var(--ease),transform .25s var(--spring),opacity .2s} +.db-col.over{transform:scale(1.008);background:var(--blush-soft);box-shadow:0 0 0 1.5px var(--blush-line);animation:dbRing .6s var(--ease)} +.db-col.colover{box-shadow:inset 3px 0 0 var(--blush),0 0 0 1px var(--line)} +.db-card{animation:dbIn .3s var(--ease) backwards;transition:box-shadow .2s var(--ease),transform .25s var(--spring),opacity .15s,background .15s} +.db-card:hover{transform:translateY(-2px);box-shadow:0 0 0 1px var(--line-2),0 10px 22px -14px rgba(0,0,0,.55)} +.db-card:active{transform:scale(.985)} +.db-card.lifted{transform:rotate(1.2deg) scale(.98)} +.db-card-title .i{transition:transform .25s var(--spring),color .15s} +.db-card:hover .db-card-title .i{transform:scale(1.15) rotate(-6deg);color:var(--blush)} + +/* gallery */ +.db-gcard{animation:dbIn .35s var(--ease) backwards;transition:box-shadow .22s var(--ease),transform .28s var(--spring)} +.db-gcard:hover{transform:translateY(-3px);box-shadow:0 0 0 1px var(--line-2),0 16px 30px -18px rgba(0,0,0,.6)} +.db-gcard:active{transform:scale(.98)} +.db-gcover{overflow:hidden} +.db-gcover img{transition:transform .5s var(--ease)} +.db-gcover>.i{transition:transform .35s var(--spring)} +.db-gcard:hover .db-gcover img{transform:scale(1.06)} +.db-gcard:hover .db-gcover>.i{transform:scale(1.15) rotate(-6deg)} +.db-gnew{transition:background .15s,color .15s,box-shadow .2s,transform .28s var(--spring)} +.db-gnew:hover{background:var(--blush-soft);color:var(--blush);box-shadow:0 0 0 1px var(--blush-line)} + +/* calendar */ +.db-day{transition:background .18s var(--ease)} +.db-day:hover{background:var(--hover)} +.db-day-add{transform:scale(.7);transition:opacity .15s,background .15s,color .15s,transform .22s var(--spring)} +.db-day:hover .db-day-add,.db-day-add:focus-visible{transform:none} +.db-day-add:hover{transform:scale(1.15) rotate(90deg)} +.db-day.today .db-day-head span{animation:dbRing 2.4s var(--ease) 1} +.db-ev{animation:dbIn .25s var(--ease) backwards;transition:background .15s,transform .2s var(--spring),box-shadow .2s var(--ease),opacity .15s} +.db-ev:hover{transform:translateX(2px)} +.db-ev:active{transform:scale(.97)} +.db-grip{transition:opacity .15s,width .15s var(--ease)} +.db-grip:hover{width:11px;opacity:.85} +.db-cal-head strong{transition:opacity .2s} + +/* timeline */ +.db-tl-bar{animation:dbIn .3s var(--ease) backwards;transition:background .15s,box-shadow .2s var(--ease),transform .2s var(--spring)} +.db-tl-bar:hover{transform:translateY(-1px);box-shadow:inset 0 0 0 1px var(--blush),0 6px 14px -8px var(--blush)} +.db-tl-bar.moving{transform:translateY(-2px) scale(1.01);box-shadow:inset 0 0 0 1.5px var(--blush),0 10px 20px -10px var(--blush)} +.db-tl-grip{transition:opacity .15s,width .15s var(--ease)} +.db-tl-grip:hover{width:10px;opacity:.9} +.db-tl-arrows path{stroke-dasharray:1;animation:dbDraw .7s var(--ease) backwards} +.db-tl-today{animation:fade .6s ease backwards} +.db-undated .db-ev.static{transition:background .15s,transform .2s var(--spring)} + +/* list */ +.db-lrow{position:relative;animation:dbIn .25s var(--ease) backwards;transition:background .15s var(--ease),padding-left .22s var(--ease)} +.db-lrow::before{content:"";position:absolute;left:0;top:7px;bottom:7px;width:2px;border-radius:2px;background:var(--blush);transform:scaleY(0);transition:transform .22s var(--ease)} +.db-lrow:hover{padding-left:12px} +.db-lrow:hover::before{transform:scaleY(1)} +.db-lrow>.i{transition:transform .25s var(--spring),color .15s} +.db-lrow:hover>.i{transform:scale(1.15) rotate(-6deg);color:var(--blush)} + +/* a database inside a page */ +.emb.live{transition:box-shadow .25s var(--ease)} +.emb.live:hover{box-shadow:inset 3px 0 0 var(--blush),0 0 0 1px var(--line-2)} + +/* import dialog */ +.csv-col{transition:background .15s var(--ease),opacity .2s} +.csv-col:hover{background:var(--hover)} +.csv-col>.i{transition:transform .22s var(--spring),color .15s} +.csv-col:hover>.i{transform:translateX(3px);color:var(--blush)} +.csv-col{animation:dbIn .3s var(--ease) backwards} + +/* a short cascade when a view appears */ +.dbt tbody tr[data-rec]:nth-child(1),.db-lrow:nth-child(1),.db-gcard:nth-child(1),.csv-col:nth-child(1),.kcards .kcard:nth-child(1){animation-delay:0ms} +.dbt tbody tr[data-rec]:nth-child(2),.db-lrow:nth-child(2),.db-gcard:nth-child(2),.csv-col:nth-child(2),.kcards .kcard:nth-child(2){animation-delay:24ms} +.dbt tbody tr[data-rec]:nth-child(3),.db-lrow:nth-child(3),.db-gcard:nth-child(3),.csv-col:nth-child(3),.kcards .kcard:nth-child(3){animation-delay:48ms} +.dbt tbody tr[data-rec]:nth-child(4),.db-lrow:nth-child(4),.db-gcard:nth-child(4),.csv-col:nth-child(4),.kcards .kcard:nth-child(4){animation-delay:72ms} +.dbt tbody tr[data-rec]:nth-child(5),.db-lrow:nth-child(5),.db-gcard:nth-child(5),.csv-col:nth-child(5),.kcards .kcard:nth-child(5){animation-delay:96ms} +.dbt tbody tr[data-rec]:nth-child(6),.db-lrow:nth-child(6),.db-gcard:nth-child(6),.csv-col:nth-child(6),.kcards .kcard:nth-child(6){animation-delay:120ms} +.dbt tbody tr[data-rec]:nth-child(7),.db-lrow:nth-child(7),.db-gcard:nth-child(7),.csv-col:nth-child(7),.kcards .kcard:nth-child(7){animation-delay:144ms} +.dbt tbody tr[data-rec]:nth-child(8),.db-lrow:nth-child(8),.db-gcard:nth-child(8),.csv-col:nth-child(8),.kcards .kcard:nth-child(8){animation-delay:168ms} + +/* database board: the Kanban board's columns and cards (kcol, khead, kcard …), with a colour pill per group */ +.db-board .kcol{width:284px} +.db-col-head{cursor:grab} +.db-col-head:active{cursor:grabbing} +.db-kh{display:flex;align-items:center;min-width:0} +.db-opt.big{height:26px;padding:0 12px;font-size:13px;font-weight:600;letter-spacing:.005em} +.db-col-title{color:var(--ink-3);font-size:13px;font-weight:600} +.kcard.db-card{flex-direction:row;align-items:flex-start;padding:9px 30px 9px 11px;cursor:pointer} +.kcard.db-card .kc-text{display:flex;flex-direction:column;gap:7px;font-size:13.5px} +.db-card-title{display:flex;align-items:center;gap:7px;font-weight:550;color:var(--ink);min-width:0} +.db-card-title .i{width:14px;height:14px;color:var(--ink-3);flex:none} +.db-card-title span{overflow:hidden;text-overflow:ellipsis} +.db-card-props{display:flex;flex-wrap:wrap;gap:4px;align-items:center;min-width:0} +.db-card-props:empty{display:none} +.db-cardedit{width:100%;height:26px;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:550 13.5px var(--f-ui)} +.db-col .kadd{margin-top:6px} +.db-menu .db-swatches{padding:6px 10px 8px} +.db-dotname{display:inline-flex;align-items:center;gap:8px;min-width:0} +/* Kanban column titles are colour pills too */ +.kh-title .db-opt{max-width:100%;overflow:hidden;text-overflow:ellipsis;display:inline-block;line-height:26px} +.kh-title:hover{background:transparent} +.kh-title:hover .db-opt{transform:translateY(-1px)} +.kmenu .mi{transition:background .12s,color .12s,padding-left .18s var(--ease)} +.kmenu .mi:hover{padding-left:13px} +.kmenu .cv-swatch{transition:transform .2s var(--spring),box-shadow .2s var(--ease)} +.kmenu .cv-swatch:hover{transform:scale(1.2)} +.kmenu .db-sub{padding-top:6px} +.kmenu .db-swatches{padding:4px 10px 8px} +/* picture covers */ +.cover.photo{background:var(--island-2)} +.cover.photo::after{display:none} +.cover-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;animation:fade .4s ease both} +.cover.over{box-shadow:inset 0 0 0 2px var(--blush)} +.cover.over::before{content:"Drop an image to use it as the cover";position:absolute;inset:0;z-index:2;display:grid;place-items:center;background:color-mix(in srgb,var(--island) 70%,transparent);color:var(--ink);font-size:13px;pointer-events:none} +/* formulas and totals */ +.db-formula{display:inline-flex;align-items:center;gap:6px;min-height:30px;padding:0 6px;font-size:13px;color:var(--ink);font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.db-formula.empty{color:var(--ink-3)} +.db-formula.err{color:var(--danger)} +.db-formula .i{width:13px;height:13px} +.db-fx-err{color:var(--danger)} +.db-formula-in{width:100%;resize:vertical;min-height:62px;border:0;outline:0;border-radius:6px;padding:8px 9px;background:var(--hover);color:var(--ink);font:12.5px/1.5 var(--f-mono)} +.db-formula-in:focus{box-shadow:inset 0 0 0 1px var(--blush-line)} +.db-formula-preview{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;padding:6px 8px 2px;font-size:12.5px;color:var(--ink-2)} +.db-formula-preview b{color:var(--ink);font-weight:600;font-variant-numeric:tabular-nums} +.db-formula-preview small{color:var(--ink-3);font-size:11.5px} +.db-formula-preview.err{color:var(--danger);align-items:flex-start;gap:6px} +.db-formula-preview.err .i{width:13px;height:13px;margin-top:2px;flex:none} +.db-help{margin:6px 4px 2px;color:var(--ink-3);font-size:12px} +.db-help summary{cursor:pointer;padding:4px 4px;border-radius:5px} +.db-help summary:hover{color:var(--ink)} +.db-help p{margin:4px 4px 6px;line-height:1.5} +.db-help code{font:11.5px var(--f-mono);color:var(--ink-2);background:var(--hover);padding:1px 4px;border-radius:4px} +.db-help-fns{font:11px/1.6 var(--f-mono);color:var(--ink-3)} +.dbt tfoot td{border-bottom:0;border-top:1px solid var(--line);padding:0 2px;position:sticky;bottom:0;background:var(--island);z-index:2} +.dbt tfoot td.db-name,.dbt tfoot td.db-selcol{z-index:3} +.db-calc{display:flex;align-items:center;gap:7px;width:100%;height:32px;padding:0 8px;border-radius:5px;font-size:12px;color:var(--ink-3);white-space:nowrap;overflow:hidden;transition:background .15s,color .15s} +.db-calc:hover{background:var(--hover);color:var(--ink)} +.db-calc-hint{opacity:0;transition:opacity .15s} +.db-calcrow:hover .db-calc-hint,.db-calc:focus-visible .db-calc-hint{opacity:.8} +.db-calc.set b{color:var(--ink);font-weight:600;font-variant-numeric:tabular-nums;margin-left:auto} +.db-calc-l{text-transform:uppercase;letter-spacing:.06em;font-size:10.5px;font-weight:600} +/* the column title is just the pill: the empty space beside it is for grabbing the column, not for renaming */ +.khead .kh-title{flex:0 1 auto;max-width:calc(100% - 96px);padding:2px 0;cursor:pointer} +.khead .kh-title:hover{background:transparent} +.khead .kh-n{margin-left:auto} +/* a Kanban card's text is rendered with the editor's blocks */ +.kc-text .eb{margin:2px 0} +.kc-text .eb:first-child{margin-top:0} +.kc-text .eb:last-child{margin-bottom:0} +.kc-text .eb-cols{margin:2px 0} +.kc-text .code,.kc-text .math,.kc-text .tbl,.kc-text .callout{margin:4px 0} +.kc-text .img img{max-width:100%;border-radius:6px} +.kc-text .b-h1 .c{font-size:1.3em} +.kc-text .b-h2 .c{font-size:1.18em} +.kc-text .b-h3 .c{font-size:1.08em} +.kcard.done .kc-text .eb .c{color:var(--ink-3);text-decoration:line-through;text-decoration-color:var(--ink-3)} +.kcard.done .kc-text .code .c,.kcard.done .kc-text .math{text-decoration:none} +/* keep a card compact: the text is as large as on the old cards, not as large as page text */ +.kc-text .eb .c,.kc-text .eb .c2{font-size:1em;line-height:1.5} +.kc-text .eb{gap:8px} +.kc-text .b-bullet .bul,.kc-text .b-number .num,.kc-text .b-todo .check{margin-top:.2em} +.kc-text .eb .c{padding-top:0;padding-bottom:0} + +/* split view: the side pane sits beside the page */ +.panes{flex:1;min-height:0;min-width:0;display:flex} +.panes > .pagewrap{flex:1 1 0;min-width:0} +.split{flex:1 1 0;min-width:0;display:flex;flex-direction:column;border-left:1px solid var(--line)} +.split-bar{display:flex;align-items:center;gap:6px;height:38px;padding:0 8px 0 12px;flex:none;color:var(--ink-2);font-size:13px} +.split-bar .crumb{flex:1;min-width:0} +.split-bar .btn-icon{width:28px;height:28px} +.split-msg{margin:auto;color:var(--ink-3);font-size:13.5px;padding:24px;text-align:center} +.split .page{max-width:var(--page-w)} +.split-title{margin:8px 0 14px;font-size:clamp(calc(var(--fs) * 1.6),3vw,calc(var(--fs) * 2.1))} +@media (max-width:900px){.panes{flex-direction:column}.split{border-left:0;border-top:1px solid var(--line)}} + +/* dragging a page from the sidebar over the page: left half or right half */ +.drop-halves{position:absolute;inset:0;z-index:60;display:flex;gap:8px;padding:8px;pointer-events:none} +.drop-halves > div{flex:1;border-radius:10px;display:grid;place-items:center;box-shadow:0 0 0 1px var(--line);color:var(--ink-3);font-size:13px;transition:background .15s,box-shadow .15s,color .15s} +.drop-halves > div.on{background:color-mix(in oklab,var(--blush) 14%,transparent);box-shadow:0 0 0 1.5px var(--blush-line);color:var(--ink)} +.pane-main{flex:1 1 0;min-width:0;min-height:0;display:flex;flex-direction:column} +.split > .cv{flex:1} +.ctx .mi-sub{position:relative} +.ctx .mi-sub>.mi>.i:last-child{margin-left:auto;width:14px;height:14px} +.ctx .mi-label{flex:1;min-width:0} +.ctx .mi-sub:hover>.mi,.ctx .mi-sub>.mi[aria-expanded="true"]{background:var(--hover);color:var(--ink)} +.ctx .sub{position:absolute;top:-6px;left:calc(100% - 4px);width:180px;transform-origin:top left} +.ctx .sub.left{left:auto;right:calc(100% - 4px);transform-origin:top right} +.ctx .ctx-input{width:100%;height:34px;padding:0 10px;border-radius:6px;background:var(--hover);color:var(--ink);box-shadow:0 0 0 1px var(--line-2)} + +.swatches{display:flex;gap:8px;flex:none} +.swatch{width:24px;height:24px;border-radius:50%;background:var(--c);box-shadow:inset 0 0 0 1px var(--line-2);display:grid;place-items:center;color:var(--ground);transition:transform .2s var(--spring)} +.swatch:hover{transform:scale(1.12)} +.swatch.on{box-shadow:0 0 0 2px var(--raise),0 0 0 4px var(--c)} +.swatch .i{width:13px;height:13px} + +.set-block{padding:12px 0;border-bottom:1px solid var(--line)} +.palettes{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px;margin-top:12px} +.pal{display:flex;flex-direction:column;gap:7px;padding:5px 5px 7px;border-radius:10px;text-align:left;color:var(--ink-2);font-size:12px;font-weight:550;box-shadow:inset 0 0 0 1px var(--line);transition:background .15s,box-shadow .15s,color .15s} +.pal i{display:block;position:relative;height:58px;border-radius:6px;background:var(--g);box-shadow:inset 0 0 0 1px var(--line-2);overflow:hidden} +.pal i::before{content:"";position:absolute;left:0;top:0;bottom:0;width:30%;background:var(--i)} +.pal i b{position:absolute;left:38%;height:5px;border-radius:3px;background:var(--t);opacity:.55} +.pal i b:nth-child(1){top:12px;width:34%;height:7px;opacity:.9} +.pal i b:nth-child(2){top:27px;width:50%} +.pal i b:nth-child(3){top:38px;width:40%;background:var(--r);opacity:1;height:9px} +.pal span{padding:0 3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.pal:hover{background:var(--hover);color:var(--ink)} +.pal.on{color:var(--ink);box-shadow:inset 0 0 0 2px var(--blush)} + +/* more settings (prefs.js) */ +:root[data-density="compact"] .row{height:27px} +:root[data-density="comfortable"] .row{height:36px} +.hide-cover .cover{display:none} +.hide-cover .p-icon{margin-top:24px} +.hide-icon .p-icon{display:none} +.hide-backlinks .backlinks{display:none} +.code-wrap pre.c{white-space:pre-wrap;overflow-x:hidden;word-break:break-word} +.typewriter .page{padding-bottom:45vh} +.set-sub{margin:10px 0 4px;color:var(--ink-3);font-size:12px} +.set-area{width:100%;min-height:96px;resize:vertical;margin-top:8px;padding:9px 11px;border:0;border-radius:7px;background:var(--hover);box-shadow:inset 0 0 0 1px var(--line);color:var(--ink);font:12.5px/1.5 var(--f-mono);user-select:text} +.set-area:focus-visible{outline:2px solid var(--blush-line)} +.set-in{width:180px;height:30px;padding:0 10px;border:0;border-radius:6px;background:var(--hover);box-shadow:inset 0 0 0 1px var(--line);color:var(--ink);font:13px var(--f-ui);user-select:text} +.set-in:focus-visible{outline:2px solid var(--blush-line)} +.set-in.num{width:84px;text-align:right;font-variant-numeric:tabular-nums} +.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px} +.chip-t{height:26px;padding:0 10px;border-radius:99px;font-size:12px;font-weight:550;color:var(--ink-3);background:var(--hover);transition:background .15s,color .15s} +.chip-t.on{color:var(--ink);background:var(--blush-soft);box-shadow:inset 0 0 0 1px var(--blush-line)} +.chip-t:not(.on){text-decoration:line-through;opacity:.8} +.keys{display:grid;grid-template-columns:1fr auto;gap:0 18px} +.keys>*{padding:7px 0;border-bottom:1px solid var(--line)} +.keys .kd{display:flex;gap:4px;justify-content:flex-end} +.words{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px} +.word{display:flex;align-items:center;gap:4px;height:26px;padding:0 4px 0 10px;border-radius:99px;background:var(--hover);font-size:12.5px} +.word button{width:20px;height:20px;border-radius:50%;display:grid;place-items:center;color:var(--ink-3)} +.word button:hover{color:var(--ink);background:var(--press)} +.word .i{width:11px;height:11px} +.danger-btn{color:var(--danger)} + +/* tasks dialog */ +.tasks-panel{width:min(720px,100%)} +.tk-bar{display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid var(--line);flex-wrap:wrap} +.tk-sel,.tk-q{height:30px;border-radius:6px;border:1px solid var(--line-2);background:var(--island-2);color:var(--ink);font:inherit;font-size:12.5px;padding:0 8px;outline:0} +.tk-q{flex:1;min-width:140px} +.tk-q:focus,.tk-sel:focus{border-color:var(--blush-line)} +.tk-list{max-height:min(520px,60vh)} +.tk-group{padding:4px 0 8px} +.tk-page{display:flex;align-items:center;gap:8px;padding:4px 10px;font-weight:600;font-size:12.5px;color:var(--ink-2);border-radius:6px} +.tk-page:hover{background:var(--hover);color:var(--ink)} +.tk{display:flex;align-items:baseline;gap:9px;padding:5px 10px;border-radius:6px;cursor:pointer} +.tk:hover{background:var(--hover)} +.tk input{accent-color:var(--blush);margin:0;align-self:center} +.tk-t{flex:0 1 auto;min-width:0} +.tk.done .tk-t{text-decoration:line-through;color:var(--ink-3)} +.tk-due{font-size:11.5px;padding:1px 7px;border-radius:99px;background:var(--hover);color:var(--ink-2);white-space:nowrap} +.tk-due.over{background:color-mix(in oklab,var(--danger) 16%,transparent);color:var(--danger)} +.tk-tag{font-size:11.5px;color:var(--ink-3)} +.unlinked .bl-loose{align-items:center;padding:4px 10px} +.unlinked .bl-open{display:flex;align-items:flex-start;gap:10px;flex:1;min-width:0;text-align:left;padding:4px 0} +.unlinked .btn{height:28px;padding:0 10px;font-size:12.5px;background:var(--hover)} +.unlinked h2 em{font-style:normal;margin-left:6px;font-weight:500} + +/* daily calendar */ +.cal-panel{width:min(320px,100%)} +.cal-top{display:flex;align-items:center;justify-content:space-between;padding:12px 12px 6px} +.cal-top b{font-weight:600} +.cal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;padding:6px 12px 10px} +.cal-h{text-align:center;font-size:11px;color:var(--ink-3);padding:4px 0} +.cal-d{position:relative;height:34px;border-radius:7px;font-size:13px;color:var(--ink-2)} +.cal-d:hover{background:var(--hover);color:var(--ink)} +.cal-d.has{color:var(--ink);font-weight:600} +.cal-d.has::after{content:"";position:absolute;left:50%;bottom:4px;width:4px;height:4px;margin-left:-2px;border-radius:50%;background:var(--blush)} +.cal-d.today{box-shadow:inset 0 0 0 1px var(--blush-line)} +.cal-d.cur{background:var(--blush-soft)} +.cal-foot{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-top:1px solid var(--line);color:var(--ink-3);font-size:12px} +.cal-dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--blush);margin-right:4px} +/* daily step buttons, tag manager */ +.day-nav{display:flex;align-items:center;gap:2px} +.day-nav .day-lbl{font-size:12.5px;color:var(--ink-2);padding:0 4px;white-space:nowrap} +.tag-dot{width:9px;height:9px;border-radius:50%;flex:none;margin:0 4px} +.tg-name{text-align:left} +.tg-edit{flex:1;min-width:0} +/* typed properties on a plain page */ +.ptype{position:fixed;z-index:90;min-width:170px;padding:5px;border-radius:9px;background:var(--raise);box-shadow:0 0 0 1px var(--line-2),var(--shadow)} +.ptype .mi{display:flex;align-items:center;gap:9px;width:100%;padding:7px 9px;border-radius:6px;text-align:left} +.ptype .mi:hover,.ptype .mi.on{background:var(--hover)} +.pk-type{display:inline-grid;place-items:center;border-radius:5px;padding:2px} +.pk-type:hover{background:var(--hover)} +.row.more{color:var(--ink-3);font-size:12.5px;width:100%;text-align:left} +.row.more:hover{color:var(--ink-2)} +.keys.rebind .kd-btn{justify-self:start;min-width:120px;padding:3px 6px;border-radius:6px;text-align:left} +.keys.rebind .kd-btn:hover{background:var(--hover)} +.keys.rebind .kd-btn.capturing{box-shadow:inset 0 0 0 1px var(--blush-line);background:var(--blush-soft)} +.keys.rebind .kd-btn em{font-style:normal;color:var(--ink-2);font-size:12.5px} +.tree:focus-visible{outline-offset:-2px} diff --git a/src/lib/components/Backlinks.svelte b/src/lib/components/Backlinks.svelte new file mode 100644 index 0000000..907a158 --- /dev/null +++ b/src/lib/components/Backlinks.svelte @@ -0,0 +1,71 @@ + + +{#if items.length} + +{/if} + +{#if loose.length} + +{/if} diff --git a/src/lib/components/Board.svelte b/src/lib/components/Board.svelte new file mode 100644 index 0000000..7fec87e --- /dev/null +++ b/src/lib/components/Board.svelte @@ -0,0 +1,1041 @@ + + + + + + +{#if menu} + +{/if} + +{#if slash?.open && editing?.kind === 'card'} +
e.preventDefault()} + > +
+ {slash.mode === 'link' ? 'Link to page' : slash.q ? 'Matching commands' : 'Formatting & blocks'} +
+ {#each slash.items as it, i} + + {/each} +
+{/if} diff --git a/src/lib/components/CanvasView.svelte b/src/lib/components/CanvasView.svelte new file mode 100644 index 0000000..5db049e --- /dev/null +++ b/src/lib/components/CanvasView.svelte @@ -0,0 +1,1307 @@ + + + + + { ctxAway(e); editAway(e); }} onblur={closeCtx} onkeydown={onWindowKey} /> + + +
0} + bind:this={el} + tabindex="0" + role="application" + aria-label="Canvas" + style="--gs:{gridStep}px;--gx:{view.x}px;--gy:{view.y}px;--iz:{1 / view.z}" + onpointerdown={onDown} + ondblclick={onDouble} + oncontextmenu={onContext} + onkeydown={onKey} + ondragover={onDragOver} + ondrop={onDrop} +> + {#if broken} +
+ +

This canvas file can’t be read. It is left exactly as it is.

+

{broken}

+
+ {:else} +
+ {#each groups as n (n.id)} +
+ + {#if n.collapsed}{heldBy.get(n.id) || 0} {(heldBy.get(n.id) || 0) === 1 ? 'item' : 'items'}{/if} + {#if editing?.id === n.id && editing.what === 'label'} + + {:else} +
{n.label || 'Group'}
+ {/if} + {#each C.SIDES as s (s)}{/each} + {#if !n.collapsed}{/if} +
+ {/each} + + + + {#each data.edges as e (e.id)} + {#if e.label || (editing?.id === e.id && editing.what === 'edge')} + {@const g = edgeGeo(e)} + {#if g} + {#if editing?.id === e.id} + + {:else} +
{e.label}
+ {/if} + {/if} + {/if} + {/each} + + {#each cards as n (n.id)} + {@const kind = n.type === 'file' ? C.fileKind(n.file) : n.type} +
+ {#if n.type === 'text'} + {#if editing?.id === n.id} + +
+ {:else} +
{#if String(n.text || '').trim()}{@html md(n.text)}{:else}Empty card{/if}
+ {/if} + {:else if kind === 'note'} + {@const p = previews[previewKey(n)]} + {@const page = pageOf(n.file)} +
+ + +
+
+ {#if !p || p.state === 'loading'}Loading… + {:else if p.state === 'missing'}This note isn’t in your notes folder. + {:else if p.state === 'board'}A board. Double-click to open it. + {:else if p.html}{@html p.html} + {:else}An empty note.{/if} +
+ {:else if kind === 'image'} + {C.fileName(n.file)} + {:else if kind === 'canvas' || kind === 'other'} +
+
{kind === 'canvas' ? 'A canvas. Double-click to open it.' : n.file}
+ {:else if n.type === 'link'} + {@const meta = webMeta[n.url]} +
+
+ {#if meta?.description}

{meta.description}

{/if} + {meta ? hostOf(n.url) : n.url} +
+ {:else} +
{n.type}
+ {/if} + {#each C.SIDES as s (s)}{/each} + +
+ {/each} + + {#each guides as g, i (i)} +
+ {/each} + + {#if box}
{/if} +
+ + {#if !data.nodes.length} +

Double-click anywhere to add a card,
or add notes and pictures from the bar below.

+ {/if} + + {#if selBar} +
+ {#each COLORS as c (c)} + + {/each} + + {#if selNodes.length > 1} + {#each ARRANGE as [how, icon, label] (how)} + + {/each} + {#if selNodes.length > 2} + + + {/if} + + {/if} + {#if oneFile}{/if} + {#if selEdges.length === 1 && !selNodes.length} + + + {/if} + +
+ {/if} + + {#if ctx} + + {/if} + + {#if find} + + {/if} + + + + + {/if} +
diff --git a/src/lib/components/ClipDialog.svelte b/src/lib/components/ClipDialog.svelte new file mode 100644 index 0000000..56ef59d --- /dev/null +++ b/src/lib/components/ClipDialog.svelte @@ -0,0 +1,35 @@ + + + e.key === 'Escape' && close()} /> + + diff --git a/src/lib/components/ContextMenu.svelte b/src/lib/components/ContextMenu.svelte new file mode 100644 index 0000000..92d2569 --- /dev/null +++ b/src/lib/components/ContextMenu.svelte @@ -0,0 +1,192 @@ + + + + + diff --git a/src/lib/components/CoverPicker.svelte b/src/lib/components/CoverPicker.svelte new file mode 100644 index 0000000..98081e6 --- /dev/null +++ b/src/lib/components/CoverPicker.svelte @@ -0,0 +1,126 @@ + + + el && !el.contains(e.target) && anchor && !anchor.contains(e.target) && onclose()} /> + + + + diff --git a/src/lib/components/CsvImportDialog.svelte b/src/lib/components/CsvImportDialog.svelte new file mode 100644 index 0000000..05e8e3e --- /dev/null +++ b/src/lib/components/CsvImportDialog.svelte @@ -0,0 +1,122 @@ + + + e.key === 'Escape' && close()} /> + + diff --git a/src/lib/components/DailyCalendar.svelte b/src/lib/components/DailyCalendar.svelte new file mode 100644 index 0000000..f4b9cbf --- /dev/null +++ b/src/lib/components/DailyCalendar.svelte @@ -0,0 +1,68 @@ + + + e.key === 'Escape' && close()} /> + + diff --git a/src/lib/components/Database.svelte b/src/lib/components/Database.svelte new file mode 100644 index 0000000..820b243 --- /dev/null +++ b/src/lib/components/Database.svelte @@ -0,0 +1,1370 @@ + + + + + + +{#snippet val(rec, p)} + {@const v = valueOf(rec, p)} + {#if p.type === 'checkbox'} + + {:else if p.type === 'people'} + {#each v as n (n)}{n.slice(0, 1).toUpperCase()}{n}{/each} + {:else if p.type === 'files'} + {#each v as f (f)}{f}{/each} + {:else if p.type === 'select' || p.type === 'multiselect'} + {#each Array.isArray(v) ? v : v ? [v] : [] as o (o)}{o}{/each} + {:else if p.type === 'relation'} + {#each v as o (o)}{o}{/each} + {:else if p.type === 'formula'} + {#if rec.fx?.[p.key]}Error{:else if v !== ''}{formulaDisplay(p, v)}{/if} + {:else if p.type === 'number' && v !== null} + {#if p.format === 'bar'} + {formatNumber(v, { format: 'commas' })} + {:else} + {formatNumber(v, p)} + {/if} + {:else if p.type === 'date' && v} + {fmtDate(v.slice(0, 10))} + {:else if v !== '' && v !== null} + {v} + {/if} +{/snippet} + +{#snippet calcCell(key, p)} + {@const r = calcFor(key, p)} + +{/snippet} + +{#snippet recordProps(rec, skip = null)} + {#each shownProps.filter((p) => !skip || p.key !== skip.key) as p (p.key)} + {@const v = valueOf(rec, p)} + {#if p.type === 'checkbox' ? v : v !== '' && v !== null && !(Array.isArray(v) && !v.length)} + {@render val(rec, p)} + {/if} + {/each} +{/snippet} + +
+ + + {#if view.type === 'table'} + {#if sel.length} + + {/if} +
+ + + + + {#each shownProps as p (p.key)}{/each} + + + + + + + {#each shownProps as p (p.key)} + + {/each} + + + + + {#each rows as rec (rec.id)} + + + + {#each shownProps as p (p.key)} + + {/each} + + + {/each} + + + + + + + + + {#each shownProps as p (p.key)}{/each} + + + +
+ + + + {#if !isEmbed} startResize(e, 'title')}>{/if} + { dragProp = p.key; e.dataTransfer.setData('text/plain', p.key); e.dataTransfer.effectAllowed = 'move'; }} + ondragover={(e) => { if (dragProp) { e.preventDefault(); overProp = p.key; } }} + ondragleave={() => (overProp = null)} + ondrop={(e) => { e.preventDefault(); dropProp(p.key); }} + ondragend={() => { dragProp = null; overProp = null; }} + > + + {#if !isEmbed} startResize(e, p.key)}>{/if} + + {#if !isEmbed}{/if} +
+ + + + + rename(rec, e.currentTarget.value)} /> + + + setValue(rec, p, v)} onaddoption={(n) => addOption(p, n)} />
{@render calcCell('title', { type: 'text', key: 'title', name: 'Name' })}{@render calcCell(p.key, p)}
+ {#if !rows.length && records.length}

No records match this view's filters.

{/if} +
+ + {:else if view.type === 'board'} + {#if !groupProp} +

A board groups records by a select or checkbox property.

+ {:else} +
+ {#each columns as col (col.id)} + {@const named = typeof col.value === 'string' && col.value !== ''} +
{ + if (dragCol) { if (named) { e.preventDefault(); overGroup = col.value; } } + else if (dragRec) { e.preventDefault(); overKey = 'c' + col.id; } + }} + ondragleave={() => { overKey = null; overGroup = null; }} + ondrop={(e) => { e.preventDefault(); drop(col); }} + > +
{ if (!(canEditGroups && named)) return; dragCol = col.value; e.dataTransfer.setData('text/plain', col.value); e.dataTransfer.effectAllowed = 'move'; }} + ondragend={() => { dragCol = null; overGroup = null; overKey = null; }} + oncontextmenu={(e) => openMenuAt('col', e, { name: col.value, title: col.title })} + > + {#if editCol && named && editCol.name === col.value} + { if (e.key === 'Enter') { e.preventDefault(); finishGroupName(); } if (e.key === 'Escape') { e.preventDefault(); editCol = null; } }} + onblur={finishGroupName} + /> + {:else} + + {/if} + {col.records.length} + + +
+
+ {#each col.records as rec (rec.id)} +
{ dragRec = rec.id; e.dataTransfer.setData('text/plain', rec.id); e.dataTransfer.effectAllowed = 'move'; }} + ondragend={() => { dragRec = null; overKey = null; }} + onclick={(e) => { if (editCard?.id !== rec.id && !e.target.closest('.kmore')) open(rec); }} + onkeydown={(e) => e.key === 'Enter' && e.target === e.currentTarget && open(rec)} + oncontextmenu={(e) => openMenuAt('card', e, { id: rec.id })} + > +
+ {#if editCard?.id === rec.id} + e.stopPropagation()} + onkeydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); finishCardName(); } if (e.key === 'Escape') { e.preventDefault(); editCard = null; } }} + onblur={finishCardName} + /> + {:else} + {rec.title} + {/if} + {@render recordProps(rec, groupProp)} +
+ +
+ {/each} +
+ +
+ {/each} + {#if canEditGroups} + + {/if} +
+ {/if} + + {:else if view.type === 'calendar'} + {#if !dateProp} +

A calendar needs a date property.

+ {:else} +
+
+ + {calTitle} + + + + + + +
+
+ {#each weekdayNames(sunday) as d (d)}
{d}
{/each} + {#each grid as week} + {#each week as day (day.iso)} + {@const items = days.get(day.iso) || []} +
= reach.start && day.iso <= reach.to} + data-iso={day.iso} + role="group" + aria-label={fmtDate(day.iso)} + ondragover={(e) => { if (dragRec) { e.preventDefault(); overKey = 'd' + day.iso; } }} + ondragleave={() => (overKey = null)} + ondrop={(e) => { e.preventDefault(); dropDay(day.iso); }} + > +
+ {day.day} + +
+ {#each items.slice(0, weekly ? 99 : 3) as rec (rec.id)} + {@const end = endOf(rec)} +
+ + {#if !end || end === day.iso} + e.preventDefault()} onpointerdown={(e) => grabEnd(e, rec)}> + {/if} +
+ {/each} + {#if !weekly && items.length > 3}+{items.length - 3} more{/if} +
+ {/each} + {/each} +
+ {#if records.some((r) => !valueOf(r, dateProp))} +

{records.filter((r) => !valueOf(r, dateProp)).length} without a date aren't shown.

+ {/if} +
+ {/if} + + {:else if view.type === 'gallery'} + + + {:else if view.type === 'timeline'} + {#if !dateProp} +

A timeline needs a date property.

+ {:else} +
+ + +
+
+
+
+ {#each monthsRow as m (m.key)}{px >= 8 ? m.label : m.label.slice(0, 3)}{/each} +
+
+ {#each { length: span } as _, i} + {@const n = tl.from + i} + {@const iso = isoOfDay(n)} + 4}>{px >= 14 || (new Date(n * 864e5).getUTCDay() === 1) ? +iso.slice(8) : ''} + {/each} +
+
+ {#if todayNum >= tl.from && todayNum <= tl.to}{/if} + {#if arrows.length} + + {/if} + {#each tl.bars as b (b.rec.id)} + {@const r = barRange(b)} +
+ + {#if (r.to - r.from + 1) * px < 110} + {b.rec.title} + {/if} +
+ {/each} +
+
+ {#if tl.undated.length} +
+ No date + {#each tl.undated as rec (rec.id)} + + + + + {/each} +
+ {/if} +
+ {/if} + + {:else} +
+ {#each rows as rec (rec.id)} +
open(rec)} onkeydown={(e) => e.key === 'Enter' && open(rec)}> + + {rec.title} + {@render recordProps(rec)} + +
+ {/each} + +
+ {/if} + + {#if !records.length && view.type !== 'table'} +

No records yet.

+ {/if} +
+ +{#if menu} + +{/if} diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte new file mode 100644 index 0000000..1211caa --- /dev/null +++ b/src/lib/components/Editor.svelte @@ -0,0 +1,34 @@ + + +
diff --git a/src/lib/components/ExportDialog.svelte b/src/lib/components/ExportDialog.svelte new file mode 100644 index 0000000..145c2ba --- /dev/null +++ b/src/lib/components/ExportDialog.svelte @@ -0,0 +1,85 @@ + + + e.key === 'Escape' && close()} /> + + diff --git a/src/lib/components/FindBar.svelte b/src/lib/components/FindBar.svelte new file mode 100644 index 0000000..279af0f --- /dev/null +++ b/src/lib/components/FindBar.svelte @@ -0,0 +1,161 @@ + + + diff --git a/src/lib/components/FolderPicker.svelte b/src/lib/components/FolderPicker.svelte new file mode 100644 index 0000000..059dd04 --- /dev/null +++ b/src/lib/components/FolderPicker.svelte @@ -0,0 +1,139 @@ + + + (open = false)} /> + +
+ + { show(); e.currentTarget.select(); }} + onclick={(e) => { if (!open) show(); e.currentTarget.select(); }} + oninput={() => { open = true; active = 0; }} + onblur={() => { if (open) commitTyped(); }} + {onkeydown} + /> + +
+ +{#if open} + +
e.preventDefault()}> + {#each rows as row, i (row.kind + (row.p ? row.p.id : row.id || ''))} + {#if row.kind === 'auto'} + + {:else if row.kind === 'page'} + {@const [a, m, b] = mark(row.p.title)} + + {:else} + + {/if} + {:else} +

No pages yet.

+ {/each} +
+{/if} diff --git a/src/lib/components/GraphView.svelte b/src/lib/components/GraphView.svelte index cba0943..1a09a5b 100644 --- a/src/lib/components/GraphView.svelte +++ b/src/lib/components/GraphView.svelte @@ -5,6 +5,7 @@ import Icon from './Icon.svelte'; import WindowControls from './WindowControls.svelte'; import { store } from '$lib/state.svelte.js'; + import { nodeColor } from '$lib/graphColors.js'; import { api, errorText } from '$lib/api.js'; import { toast } from '$lib/toast.svelte.js'; import { createSimulation, neighbourhood } from '$lib/graph.js'; @@ -49,6 +50,18 @@ const ids = new Set(nodes.map((n) => n.id)); let edges = data.edges.filter(([a, b]) => ids.has(a) && ids.has(b)).map((e) => [...e, 'link']); if (g.folders) for (const n of nodes) if (n.parent && ids.has(n.parent)) edges.push([n.id, n.parent, 'tree']); + if (g.tags) { + // each tag becomes a node its pages hang from + const tagNodes = new Map(); + for (const n of nodes) { + if (n.ghost) continue; + for (const t of n.tags || []) { + if (!tagNodes.has(t)) tagNodes.set(t, { id: '#' + t, title: '#' + t, tags: [], ghost: true, tag: t }); + edges.push([n.id, '#' + t, 'tree']); + } + } + nodes = nodes.concat([...tagNodes.values()]); + } if (g.local && cur && ids.has(cur)) { const keep = neighbourhood(cur, edges, g.depth || 1); nodes = nodes.filter((n) => keep.has(n.id)); @@ -62,12 +75,14 @@ sim.params.repel = (store.graph.repel || 420) * g.spacing; sim.params.linkDistance = (store.graph.linkDistance || 96) * g.spacing; sim.setData(nodes, edges); + // a big graph cools faster (about 120 ticks to rest instead of 300) + sim.alphaDecay = 1 - Math.pow(0.002, 1 / (nodes.length > 3000 ? 120 : 300)); const kind = new Map(edges.map(([a, b, k]) => [a + '\u0000' + b, k])); for (const l of sim.links) l.kind = kind.get(l.source.id + '\u0000' + l.target.id) || 'link'; counts = { nodes: nodes.filter((n) => !n.ghost).length, links: sim.links.filter((l) => l.kind === 'link').length }; if (!fitted) { // A head start so the first frame isn't a tangle; then frame it. - sim.tick(Math.min(220, Math.max(60, 120000 / Math.max(1, nodes.length)))); + sim.tickFor(Math.min(220, Math.max(60, 120000 / Math.max(1, nodes.length))), 350); fitted = fit(false); } sim.reheat(0.5); @@ -86,17 +101,12 @@ }; } $effect(() => { store.theme; readColors(); draw(); }); + // colouring a tag while the graph is open + $effect(() => { store.tagColors; redraw(); }); - const colorForNode = (n) => { - if (n.tags && n.tags.length > 0) { - // Pick a consistent color based on the first tag's name - let hash = 0; - for (let i = 0; i < n.tags[0].length; i++) hash = (hash << 5) - hash + n.tags[0].charCodeAt(i); - return colors.tags[Math.abs(hash) % colors.tags.length]; - } - return colors.ink3; // Default - }; + // A tag with a colour (see the sidebar's tag menu) gives its pages that colour here; the rest are plain. + const colorForNode = (n) => nodeColor(n.tags, store.tagColors, colors.ink3); const radius = (n) => (n.ghost ? 3.5 : 3.6 + Math.sqrt(n.degree) * 1.7); @@ -185,7 +195,7 @@ /* animation: run the simulation while it's warm or something is being dragged */ function frame() { raf = 0; - if (sim.running) sim.tick(sim.nodes.length > 1500 ? 1 : 2); + if (sim.running) sim.tickFor(sim.nodes.length > 1500 ? 1 : 2, 14); draw(); if (sim.running || gesture?.kind === 'drag') start(); } @@ -279,6 +289,7 @@ } } function open(n) { + if (n.tag) { store.filterTag = n.tag; store.closeGraph(); return; } if (n.ghost) store.openLink(n.title); else store.openPage(n.id); } @@ -375,6 +386,7 @@ {/if} +
Spacing diff --git a/src/lib/components/HistoryDialog.svelte b/src/lib/components/HistoryDialog.svelte new file mode 100644 index 0000000..64e91e0 --- /dev/null +++ b/src/lib/components/HistoryDialog.svelte @@ -0,0 +1,85 @@ + + + e.key === 'Escape' && close()} /> + + diff --git a/src/lib/components/Icon.svelte b/src/lib/components/Icon.svelte new file mode 100644 index 0000000..c02946f --- /dev/null +++ b/src/lib/components/Icon.svelte @@ -0,0 +1,13 @@ + + +{#if !PATHS[name] && isEmoji(name)} + +{:else} + +{/if} diff --git a/src/lib/components/IconPicker.svelte b/src/lib/components/IconPicker.svelte new file mode 100644 index 0000000..0d54c3f --- /dev/null +++ b/src/lib/components/IconPicker.svelte @@ -0,0 +1,110 @@ + + + + + diff --git a/src/lib/components/Outline.svelte b/src/lib/components/Outline.svelte new file mode 100644 index 0000000..1d0a126 --- /dev/null +++ b/src/lib/components/Outline.svelte @@ -0,0 +1,103 @@ + + + diff --git a/src/lib/components/PageView.svelte b/src/lib/components/PageView.svelte index e7fa6b2..4bb3eb0 100644 --- a/src/lib/components/PageView.svelte +++ b/src/lib/components/PageView.svelte @@ -6,9 +6,14 @@ import Board from './Board.svelte'; import Outline from './Outline.svelte'; import { store, iconOf, coverOf } from '$lib/state.svelte.js'; - import { COVERS } from '$lib/icons.js'; + import { COVERS, isImageCover } from '$lib/icons.js'; + import { tagHex } from '$lib/tagColors.js'; import IconPicker from './IconPicker.svelte'; - import { api } from '$lib/api.js'; + import Database from './Database.svelte'; + import PropCell from './PropCell.svelte'; + import { api, errorText } from '$lib/api.js'; + import { toast } from '$lib/toast.svelte.js'; + import { read, write, typeInfo, computeFormulas, TYPES } from '$lib/database.js'; let doc = $derived(store.doc); let page = $derived(store.current); @@ -17,6 +22,27 @@ let tags = $derived(Array.isArray(doc?.meta.tags) ? doc.meta.tags : []); let created = $derived(doc?.meta.created || page?.created || null); + // When this page is a record of a database: that database's properties, shown here with their own editors. + let schema = $state(null); // { dbId, properties } + $effect(() => { + const parent = page && page.parent; + const owner = parent && store.pages.find((x) => x.id === parent); + const mine = doc && doc.id; + if (!owner || !owner.db || store.isDatabase) { schema = null; return; } + store.readSchema(parent).then((s) => { if (store.doc && store.doc.id === mine) schema = s ? { dbId: parent, properties: s.properties } : null; }); + }); + const schemaKeys = $derived(new Set((schema ? schema.properties : []).map((p) => p.key))); + // formula properties of the database, worked out for this record + const formulas = $derived(schema && doc ? computeFormulas(schema.properties, { title: doc.title, props: doc.meta }) : {}); + async function addSchemaOption(p, name) { + const s = await store.readSchema(schema.dbId); + const q = s && s.properties.find((x) => x.key === p.key); + if (!q || (q.options || []).includes(name)) return; + (q.options ||= []).push(name); + await store.saveSchema(schema.dbId, s); + schema = { dbId: schema.dbId, properties: s.properties }; + } + let scrollerEl = $state(); let titleEl = $state(); let iconEl = $state(); @@ -40,7 +66,7 @@ const fmtDate = (d) => { if (!d) return '—'; const t = new Date(String(d).length === 10 ? d + 'T12:00:00' : d); - return isNaN(t) ? String(d) : t.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); + return isNaN(t) ? String(d) : t.toLocaleDateString(store.prefs.locale || undefined, { month: 'short', day: 'numeric', year: 'numeric' }); }; function restart(el, cls) { @@ -55,6 +81,37 @@ restart(iconEl, 'spin'); iconEl?.focus(); } + let coverBroken = $state(false); + const coverImage = $derived(isImageCover(doc?.meta.cover) ? doc.meta.cover : null); + $effect(() => { coverImage; coverBroken = false; }); + // Pick a picture with the file dialog, or take a dropped one: it is saved next to the page like any image. + async function uploadCover() { + try { + const name = await api.pickImage(doc.id); + if (name) setCover(name); + } catch (err) { + toast(errorText(err), 'x'); + } + } + // A picture from a web address: downloaded by the app and saved next to the page. Throws a message for the picker. + async function coverFromUrl(address) { + const name = await api.saveImageFromUrl(doc.id, address); + if (name) setCover(name); + } + async function dropCover(e) { + const file = [...(e.dataTransfer?.files || [])].find((f) => /^image\//.test(f.type)); + if (!file) return; + e.preventDefault(); + coverOver = false; + try { + if (file.size > 50 * 1024 * 1024) throw new Error('That image is too big (50 MB at most).'); + const name = await api.saveImage(doc.id, file.name || 'cover.png', new Uint8Array(await file.arrayBuffer())); + if (name) setCover(name); + } catch (err) { + toast(errorText(err), 'x'); + } + } + let coverOver = $state(false); function setCover(v) { pickingCover = false; store.setMeta({ cover: v }); @@ -113,6 +170,52 @@ let addingProp = $state(false); let newPropEl = $state(); + /* A plain page's own properties are typed by their value: true/false is a checkbox, a number a number, YYYY-MM-DD a + date, a list a multi-select, an address a link. Nothing but the value is stored, so Obsidian reads them as is. */ + const PLAIN_TYPES = ['text', 'number', 'date', 'checkbox', 'multiselect', 'url']; + const HIDDEN_KEYS = ['icon', 'cover', 'tags', 'created', 'ink-colors', 'kanban-plugin', 'ink-database', 'source']; + function inferType(v) { + if (typeof v === 'boolean' || /^(true|false)$/i.test(String(v))) return 'checkbox'; + if (Array.isArray(v)) return 'multiselect'; + if (typeof v === 'number' || /^-?\d+(\.\d+)?$/.test(String(v).trim())) return 'number'; + if (/^\d{4}-\d{2}-\d{2}/.test(String(v))) return 'date'; + if (/^https?:\/\/\S+$/i.test(String(v).trim())) return 'url'; + return 'text'; + } + const customKeys = $derived(doc ? Object.keys(doc.meta).filter((k) => doc.meta[k] !== undefined && !HIDDEN_KEYS.includes(k) && !schemaKeys.has(k)) : []); + // the values other pages give this key, as the choices of a list property + function optionsFor(k) { + const seen = new Set(); + for (const p of store.pages) { + const v = p.props && p.props[k]; + for (const x of Array.isArray(v) ? v : []) if (typeof x === 'string' && x.trim()) seen.add(x.trim()); + } + const mine = doc.meta[k]; + for (const x of Array.isArray(mine) ? mine : []) seen.add(String(x)); + return [...seen].sort((a, b) => a.localeCompare(b)); + } + const propFor = (k) => { + const type = inferType(doc.meta[k]); + return { key: k, name: k, type, options: type === 'multiselect' ? optionsFor(k) : [] }; + }; + const DEFAULTS = { text: '', number: '0', date: () => new Date().toISOString().slice(0, 10), checkbox: 'false', multiselect: [], url: '' }; + const startValue = (t) => (typeof DEFAULTS[t] === 'function' ? DEFAULTS[t]() : DEFAULTS[t]); + function convertValue(v, from, to) { + if (to === 'multiselect') return Array.isArray(v) ? v : String(v ?? '').split(',').map((x) => x.trim()).filter(Boolean); + const plain = Array.isArray(v) ? v.join(', ') : String(v ?? ''); + if (to === 'checkbox') return /^(true|yes|1|x|on)$/i.test(plain) ? 'true' : 'false'; + if (to === 'number') { const n = Number(plain.replace(',', '.')); return Number.isFinite(n) && plain.trim() ? String(n) : '0'; } + if (to === 'date') return /^\d{4}-\d{2}-\d{2}/.test(plain) ? plain.slice(0, 10) : startValue('date'); + return plain; + } + let typeMenu = $state(null); // { key, x, y } for changing a property's type + let newType = $state('text'); + function setType(k, to) { + const from = inferType(doc.meta[k]); + typeMenu = null; + if (from !== to) store.setMeta({ [k]: convertValue(doc.meta[k], from, to) }); + } + function updateMeta(k, v) { if (Array.isArray(doc.meta[k])) { store.setMeta({ [k]: v.split(',').map(s => s.trim()).filter(Boolean) }); @@ -141,13 +244,15 @@ function commitNewProp(k) { k = k.trim().replace(/[^a-zA-Z0-9_-]/g, '-'); - if (k && !doc.meta[k] && !['icon', 'cover', 'tags', 'created'].includes(k)) { - store.setMeta({ [k]: '' }); + if (k && doc.meta[k] === undefined && !HIDDEN_KEYS.includes(k)) { + store.setMeta({ [k]: startValue(newType) }); } addingProp = false; } + { if (typeMenu && !e.target.closest?.('.ptype, .pk-type')) typeMenu = null; }} /> +
{#if doc} @@ -157,12 +262,26 @@ {/each} {#key doc.version} -
+ -
+
-

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

+ {#if !store.isDatabase}
+ {#if schema} + {#each schema.properties as p (p.key)} +
+ {p.name} +
+ r.id !== doc.id).map((r) => r.title) : []} + plain + onchange={(v) => store.setMeta({ [p.key]: write(v, p.type) })} + onaddoption={(n) => addSchemaOption(p, n)} + /> +
+
+ {/each} + {/if}
Tags
{#each tags as t (t)} - + { e.preventDefault(); store.menu = { x: e.clientX, y: e.clientY, tag: t }; }} + > {t} {/each} @@ -212,44 +358,75 @@ File
- {#each Object.keys(doc.meta).filter(k => doc.meta[k] !== undefined && !['icon', 'cover', 'tags', 'created'].includes(k)) as pk (pk)} + {#each customKeys as pk (pk)} + {@const cp = propFor(pk)}
- + renameMeta(pk, e.target.value)} />
- updateMeta(pk, e.target.value)} - placeholder="Empty" - /> + {#if cp.type === 'text'} + updateMeta(pk, e.target.value)} placeholder="Empty" /> + {:else} +
+ store.setMeta({ [pk]: write(v, cp.type) })} + onaddoption={() => {}} + /> +
+ {/if}
{/each} {#if addingProp}
- { if (e.key === 'Enter') commitNewProp(e.target.value); if (e.key === 'Escape') addingProp = false; }} onblur={(e) => commitNewProp(e.target.value)} /> + { if (e.key === 'Enter') commitNewProp(e.target.value); if (e.key === 'Escape') addingProp = false; }} onblur={(e) => { if (!e.relatedTarget?.classList?.contains('type-chip')) commitNewProp(e.target.value); }} />
{/if}
-
+
+ {#if addingProp} + {#each PLAIN_TYPES as t (t)} + + {/each} + {:else} + + {/if} +
+ {/if}
- {#if !store.isBoard} + {#if !store.isBoard && !store.isDatabase} {/if}
+ {#if typeMenu} + + {/if} {#if pickingCover} { pickingCover = false; coverBtnEl?.focus(); }} /> @@ -263,7 +440,10 @@ onclose={() => { picking = false; iconEl?.focus(); }} /> {/if} - {#if store.isBoard} + {#if store.isDatabase} + +
+ {:else if store.isBoard}
{/if} @@ -275,5 +455,5 @@
{/if}
-{#if store.outlineOpen && doc && !store.isBoard}{/if} +{#if store.outlineOpen && doc && !store.isBoard && !store.isDatabase}{/if}
diff --git a/src/lib/components/Palette.svelte b/src/lib/components/Palette.svelte new file mode 100644 index 0000000..8da78c1 --- /dev/null +++ b/src/lib/components/Palette.svelte @@ -0,0 +1,170 @@ + + + diff --git a/src/lib/components/PropCell.svelte b/src/lib/components/PropCell.svelte new file mode 100644 index 0000000..2deb87e --- /dev/null +++ b/src/lib/components/PropCell.svelte @@ -0,0 +1,192 @@ + + + + +{#if prop.type === 'formula'} + + {#if error}Error{:else if value === 'true' || value === 'false'}{:else}{formulaDisplay(prop, value)}{/if} + +{:else if prop.type === 'checkbox'} + +{:else if prop.type === 'select' || prop.type === 'multiselect' || isRelation} + + {#if open} +
+ +
+ {#if prop.type === 'select' && value} + + {/if} + {#each shown as o (o)} + + {/each} + {#if canCreate} + + {:else if !shown.length} +
{isRelation ? 'No other records.' : 'No options yet. Type a name and press Enter.'}
+ {/if} +
+
+ {/if} +{:else if prop.type === 'people'} +
+ {#each chosen as n (n)} + {n.slice(0, 1).toUpperCase()}{n} + {/each} + {#if adding} + { if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); addPerson(); } if (e.key === 'Escape') { draft = ''; adding = false; } }} + onblur={addPerson} + /> + {#each suggestions.filter((s) => !chosen.includes(s)) as s (s)}{/each} + {:else} + + {/if} +
+{:else if prop.type === 'files'} +
+ {#each chosen as f (f)} + + {/each} + +
+{:else if showsFormat && !editingNumber} + +{:else if prop.type === 'number'} + (editingNumber = false)} + onchange={(e) => onchange(e.currentTarget.value === '' ? null : Number(e.currentTarget.value))} + /> +{:else if prop.type === 'date'} + onchange(e.currentTarget.value)} /> +{:else} + onchange(e.currentTarget.value)} /> +{/if} diff --git a/src/lib/components/SelectMenu.svelte b/src/lib/components/SelectMenu.svelte new file mode 100644 index 0000000..505fb1c --- /dev/null +++ b/src/lib/components/SelectMenu.svelte @@ -0,0 +1,127 @@ + + + (open = false)} onpointerdown={(e) => { if (open && !box?.contains(e.target) && !trigger?.contains(e.target)) close(false); }} /> + + + +{#if open} +
+ {#if searchable} +
+ + (active = 0)} /> +
+ {/if} + +
+{/if} diff --git a/src/lib/components/SettingsDialog.svelte b/src/lib/components/SettingsDialog.svelte new file mode 100644 index 0000000..a921c36 --- /dev/null +++ b/src/lib/components/SettingsDialog.svelte @@ -0,0 +1,587 @@ + + + e.key === 'Escape' && !capturing && close()} /> + +{#snippet toggle(label, desc, on, flip)} +
+
{label}{#if desc}{desc}{/if}
+ +
+{/snippet} + +{#snippet seg(label, desc, options, value, pick)} +
+
{label}{#if desc}{desc}{/if}
+
+ {#each options as [v, text] (v)} + + {/each} +
+
+{/snippet} + + + +{#snippet spellcheck()} +
+
Check spellingUnderlines misspelled words; right-click one for suggestions.
+ +
+ {#if spell?.enabled} + {#if spell.system} +

Spelling follows your system's languages here.

+ {:else} +
+
Language
+ ({ value: l.code, label: l.name, detail: l.code }))} + onchange={setMain} + /> +
+
+
Also checkA second language, for notes that mix two.
+ l.code !== spell.languages[0]).map((l) => ({ value: l.code, label: l.name, detail: l.code }))]} + onchange={setSecond} + /> +
+

Dictionaries are downloaded once per language, then work offline.

+ {/if} + {/if} + {#if isDesktop} +
+
Custom dictionaryWords you added with “Add to dictionary”. They are never underlined.
+
{ e.preventDefault(); addWord(); }}> + + +
+ {#if words.length} +
+ {#each words as w (w)} + {w} + {/each} +
+ {/if} +
+ {/if} +{/snippet} diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte new file mode 100644 index 0000000..3c648aa --- /dev/null +++ b/src/lib/components/Sidebar.svelte @@ -0,0 +1,302 @@ + + + diff --git a/src/lib/components/SplitPane.svelte b/src/lib/components/SplitPane.svelte new file mode 100644 index 0000000..3e25ab1 --- /dev/null +++ b/src/lib/components/SplitPane.svelte @@ -0,0 +1,176 @@ + + +
+
+ + + +
+ {#if doc && kind === 'canvas'} + {#key version}{/key} + {:else} +
+ {#if doc && kind !== 'page'} +
+

{page ? page.title : id}

+
+ {#key version} + {#if kind === 'board'} + + {:else} + + {/if} + {/key} + {:else if doc} +
+

{page ? page.title : id}

+
+
+ {/if} +
+ {/if} +
diff --git a/src/lib/components/TagsDialog.svelte b/src/lib/components/TagsDialog.svelte new file mode 100644 index 0000000..6aa0438 --- /dev/null +++ b/src/lib/components/TagsDialog.svelte @@ -0,0 +1,68 @@ + + + e.key === 'Escape' && (editing ? (editing = null) : close())} /> + + diff --git a/src/lib/components/TasksDialog.svelte b/src/lib/components/TasksDialog.svelte new file mode 100644 index 0000000..992a1b5 --- /dev/null +++ b/src/lib/components/TasksDialog.svelte @@ -0,0 +1,139 @@ + + + e.key === 'Escape' && close()} /> + + diff --git a/src/lib/components/TemplateDialog.svelte b/src/lib/components/TemplateDialog.svelte new file mode 100644 index 0000000..58edf2f --- /dev/null +++ b/src/lib/components/TemplateDialog.svelte @@ -0,0 +1,127 @@ + + + + + diff --git a/src/lib/components/TextMenu.svelte b/src/lib/components/TextMenu.svelte new file mode 100644 index 0000000..5d122b3 --- /dev/null +++ b/src/lib/components/TextMenu.svelte @@ -0,0 +1,158 @@ + + + + +{#if rows.length || formats} + +{/if} diff --git a/src/lib/components/Toast.svelte b/src/lib/components/Toast.svelte new file mode 100644 index 0000000..a216c52 --- /dev/null +++ b/src/lib/components/Toast.svelte @@ -0,0 +1,11 @@ + + +
+ {toastState.msg} + {#if toastState.action} + + {/if} +
diff --git a/src/lib/components/Topbar.svelte b/src/lib/components/Topbar.svelte index ab4fbc6..54fce1c 100644 --- a/src/lib/components/Topbar.svelte +++ b/src/lib/components/Topbar.svelte @@ -12,6 +12,9 @@ let trail = $derived(store.currentId ? [...store.ancestors(store.currentId), store.current].filter(Boolean) : []); + // the database this page is a record of + let owner = $derived(store.current && store.current.parent ? store.pages.find((p) => p.id === store.current.parent && p.db) || null : null); + function toggleMenu() { menuOpen = !menuOpen; armed = false; @@ -25,7 +28,11 @@ } menuOpen = false; if (kind === 'md' || kind === 'html' || kind === 'pdf') store.exportFormat = kind; + if (kind === 'png' || kind === 'svg' || kind === 'cpdf') store.canvasView?.export(kind === 'cpdf' ? 'pdf' : kind); if (kind === 'board') store.setBoard(!store.isBoard); + if (kind === 'csv') store.exportCsv(); + if (kind === 'importCsv') store.pickCsv(store.currentId); + if (kind === 'moveOut' && owner) store.movePage(store.currentId, owner.parent); if (kind === 'reveal') api.reveal(store.currentId); if (kind === 'history') { store.historyOpen = store.currentId; } if (kind === 'path') { @@ -43,29 +50,64 @@
+ + {#if store.currentDay} +
+ + + +
+ {/if} {#if store.saving || store.justSaved}
{store.saving ? 'Saving…' : 'Saved to disk'}
{/if} - {store.words} {store.words === 1 ? 'word' : 'words'} + {#if !store.isCanvas && !store.isDatabase} + {store.words} {store.words === 1 ? 'word' : 'words'}{#if store.words >= 100} · {Math.max(1, Math.round(store.words / 230))} min{/if} + {/if} +
+ {/if} + {#if store.isDatabase} + + + {:else} - + {/if} + {#if owner} + + {/if} + {:else} + + + +
+ {/if} {#if isDesktop} {/if} diff --git a/src/lib/components/TrashDialog.svelte b/src/lib/components/TrashDialog.svelte new file mode 100644 index 0000000..f6e11a8 --- /dev/null +++ b/src/lib/components/TrashDialog.svelte @@ -0,0 +1,89 @@ + + + e.key === 'Escape' && close()} /> + + diff --git a/src/lib/components/TreeNode.svelte b/src/lib/components/TreeNode.svelte new file mode 100644 index 0000000..3403556 --- /dev/null +++ b/src/lib/components/TreeNode.svelte @@ -0,0 +1,133 @@ + + +
+
{ if (renaming) return; if (e.altKey && page.exists) store.openSplit(page.id); else store.openPage(page.id); }} + {onkeydown} + {oncontextmenu} + > + + + {#if renaming} + e.stopPropagation()} + onkeydown={(e) => { + if (e.key === 'Enter') { e.preventDefault(); finishRename(e.currentTarget, true); } + if (e.key === 'Escape') { e.preventDefault(); finishRename(e.currentTarget, false); } + }} + onblur={(e) => finishRename(e.currentTarget, true)} + /> + {:else} + {name} + {/if} + {#if !isCanvasId(page.id)}{/if} +
+ {#if kids.length && (open || seen)} +
+
+ {#each shown as kid (kid.id)} + + {/each} + {#if shown.length < kids.length} + + {/if} +
+
+ {/if} +
diff --git a/src/lib/components/Welcome.svelte b/src/lib/components/Welcome.svelte new file mode 100644 index 0000000..a2fd7fc --- /dev/null +++ b/src/lib/components/Welcome.svelte @@ -0,0 +1,18 @@ + + +
+ +
+ +

Where should your notes live?

+

Every page is a plain Markdown file in a folder you pick. Nothing leaves this computer, and an existing Obsidian vault works too.

+
+ + +
+
+
diff --git a/src/lib/components/WindowControls.svelte b/src/lib/components/WindowControls.svelte new file mode 100644 index 0000000..e4f4794 --- /dev/null +++ b/src/lib/components/WindowControls.svelte @@ -0,0 +1,25 @@ + + +{#if show} +
+ + + +
+{/if} diff --git a/src/routes/+layout.js b/src/routes/+layout.js new file mode 100644 index 0000000..6f10b29 --- /dev/null +++ b/src/routes/+layout.js @@ -0,0 +1,3 @@ +// Desktop SPA: no server rendering, no prerendering. adapter-static writes index.html as the fallback. +export const ssr = false; +export const prerender = false; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..764f102 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,11 @@ + + +{@render children()} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index b89c7fe..db805eb 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -3,6 +3,7 @@ import Sidebar from '$lib/components/Sidebar.svelte'; import Topbar from '$lib/components/Topbar.svelte'; import PageView from '$lib/components/PageView.svelte'; + import SplitPane from '$lib/components/SplitPane.svelte'; import Palette from '$lib/components/Palette.svelte'; import ExportDialog from '$lib/components/ExportDialog.svelte'; import Toast from '$lib/components/Toast.svelte'; @@ -10,9 +11,17 @@ import TextMenu from '$lib/components/TextMenu.svelte'; import FindBar from '$lib/components/FindBar.svelte'; import GraphView from '$lib/components/GraphView.svelte'; + import CanvasView from '$lib/components/CanvasView.svelte'; import TrashDialog from '$lib/components/TrashDialog.svelte'; + import { actionFor } from '$lib/shortcuts.js'; + import { installDialogFocus } from '$lib/a11y.js'; + import TasksDialog from '$lib/components/TasksDialog.svelte'; + import TagsDialog from '$lib/components/TagsDialog.svelte'; + import DailyCalendar from '$lib/components/DailyCalendar.svelte'; + import ClipDialog from '$lib/components/ClipDialog.svelte'; import HistoryDialog from '$lib/components/HistoryDialog.svelte'; import TemplateDialog from '$lib/components/TemplateDialog.svelte'; + import CsvImportDialog from '$lib/components/CsvImportDialog.svelte'; import SettingsDialog from '$lib/components/SettingsDialog.svelte'; import Welcome from '$lib/components/Welcome.svelte'; import { store } from '$lib/state.svelte.js'; @@ -21,45 +30,127 @@ onMount(() => { store.init(); + const offDialogs = installDialogFocus(); // Right-click: remember what was clicked; if nothing in the page handled it, Electron reports it back and // the app's own menu opens (the browser preview keeps the browser's menu). let target = null; const remember = (e) => { target = e.target; }; window.addEventListener('contextmenu', remember, true); const off = api.onContextMenu?.((info) => { store.textMenu = { ...info, target }; }); - return () => { window.removeEventListener('contextmenu', remember, true); off?.(); }; + + // Mouse back / forward buttons (buttons 3 and 4) + const onMouseNav = (e) => { + if (e.button === 3) { + e.preventDefault(); + e.stopPropagation(); + store.back(); + } else if (e.button === 4) { + e.preventDefault(); + e.stopPropagation(); + store.forward(); + } + }; + const onMousePrevent = (e) => { + if (e.button === 3 || e.button === 4) { + e.preventDefault(); + e.stopPropagation(); + } + }; + window.addEventListener('mouseup', onMouseNav, true); + window.addEventListener('mousedown', onMousePrevent, true); + window.addEventListener('pointerdown', onMousePrevent, true); + window.addEventListener('auxclick', onMousePrevent, true); + + const offNav = api.onNav?.((dir) => { + if (dir === 'back') store.back(); + else if (dir === 'forward') store.forward(); + }); + + return () => { + window.removeEventListener('contextmenu', remember, true); + window.removeEventListener('mouseup', onMouseNav, true); + window.removeEventListener('mousedown', onMousePrevent, true); + window.removeEventListener('pointerdown', onMousePrevent, true); + window.removeEventListener('auxclick', onMousePrevent, true); + off?.(); + offNav?.(); + offDialogs?.(); + }; }); function onkeydown(e) { + if (e.key === 'Escape' && store.peek) store.peekEscape(); + + // Back / Forward keyboard shortcuts + if (e.altKey && !e.ctrlKey && !e.metaKey && e.key === 'ArrowLeft') { + e.preventDefault(); + store.back(); + return; + } + if (e.altKey && !e.ctrlKey && !e.metaKey && e.key === 'ArrowRight') { + e.preventDefault(); + store.forward(); + return; + } + if (platform === 'darwin' && e.metaKey && !e.altKey && !e.ctrlKey && e.key === '[') { + e.preventDefault(); + store.back(); + return; + } + if (platform === 'darwin' && e.metaKey && !e.altKey && !e.ctrlKey && e.key === ']') { + e.preventDefault(); + store.forward(); + return; + } const mod = e.ctrlKey || e.metaKey; if (!mod || !store.vault) return; - const k = e.key.toLowerCase(); - if (k === 'k') { e.preventDefault(); store.paletteOpen = !store.paletteOpen; } - else if (k === 'o' && e.shiftKey) { e.preventDefault(); if (store.doc && !store.isBoard && store.view === 'page') store.toggleOutline(); } - else if (k === 'g' && !e.shiftKey) { e.preventDefault(); store.view === 'graph' ? store.closeGraph() : store.openGraph(); } - else if (k === ',') { e.preventDefault(); store.settingsOpen = !store.settingsOpen; } - else if (k === 'd' && e.shiftKey) { e.preventDefault(); store.openDaily(); } - else if (k === 'n') { e.preventDefault(); store.templatesOpen = { parent: null }; } - else if (k === '\\') { e.preventDefault(); store.toggleSidebar(); } - else if (k === 'f' && store.doc && store.view === 'page') { e.preventDefault(); store.findOpen = (store.findOpen || 0) + 1; } - else if (k === 's' && !e.shiftKey) { e.preventDefault(); store.flush().then(() => toast('Saved to disk')); } + switch (actionFor(e, store.prefs.shortcuts)) { + case 'palette': e.preventDefault(); store.paletteOpen = !store.paletteOpen; break; + case 'outline': e.preventDefault(); if (store.doc && !store.isBoard && !store.isDatabase && !store.isCanvas && store.view === 'page') store.toggleOutline(); break; + case 'tasks': e.preventDefault(); store.tasksOpen = !store.tasksOpen; break; + case 'graph': e.preventDefault(); store.view === 'graph' ? store.closeGraph() : store.openGraph(); break; + case 'settings': e.preventDefault(); store.settingsOpen = !store.settingsOpen; break; + case 'daily': e.preventDefault(); store.openDaily(); break; + case 'newPage': e.preventDefault(); store.templatesOpen = { parent: null }; break; + case 'split': e.preventDefault(); if (store.doc && store.view === 'page') store.toggleSplit(); break; + case 'sidebar': e.preventDefault(); store.toggleSidebar(); break; + case 'find': if (store.doc && store.view === 'page' && !store.isCanvas) { e.preventDefault(); store.findOpen = (store.findOpen || 0) + 1; } break; + case 'save': e.preventDefault(); store.flush().then(() => toast('Saved to disk')); break; + } } - window.innerWidth > 760 && (store.drawer = false)} /> + !e.relatedTarget && store.peekWindowLeft()} onblur={() => store.peekWindowLeft()} onpointermove={(e) => store.collapsed && store.peekMoved(e.clientX, e.clientY)} onresize={() => window.innerWidth > 760 && (store.drawer = false)} /> {#if store.vault}
-
+
+ {#if store.collapsed && !store.focusMode} + + + {/if}
+ {#if store.splitHint && store.view === 'page'} + + {/if} {#if store.view === 'graph'} {:else} - {#if store.findOpen}{/if} - + {#if store.findOpen && !store.isCanvas}{/if} +
+ {#if store.isCanvas} +
{#key store.doc.version}{/key}
+ {:else} + + {/if} + {#if store.split}{#key store.split}{/key}{/if} +
{/if}
@@ -72,7 +163,12 @@ {#if store.menu}{/if} {#if store.textMenu}{#key store.textMenu}{/key}{/if} {#if store.trashOpen}{/if} +{#if store.tasksOpen}{/if} +{#if store.tagManagerOpen}{/if} +{#if store.calendarOpen}{/if} +{#if store.clipOpen}{/if} {#if store.historyOpen}{/if} {#if store.templatesOpen}{/if} +{#if store.csvImport}{/if} {#if store.settingsOpen}{/if}