From 27fafa0bcd8a5c207ce3493f400927d292efde6c Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 15 Oct 2026 11:57:09 +0200 Subject: [PATCH] feat(ui): components, routes and styles Sidebar (lazy tree, keyboard navigation, saved searches), page view with typed properties, tasks, tags, calendar and clip dialogs, settings with shortcut rebinding, canvas, database and board views. Co-Authored-By: Claude Sonnet 5.5 --- src/app.css | 988 ++++++++++++++- src/lib/components/Backlinks.svelte | 71 ++ src/lib/components/Board.svelte | 1041 ++++++++++++++++ src/lib/components/CanvasView.svelte | 1307 ++++++++++++++++++++ src/lib/components/ClipDialog.svelte | 35 + src/lib/components/ContextMenu.svelte | 192 +++ src/lib/components/CoverPicker.svelte | 126 ++ src/lib/components/CsvImportDialog.svelte | 122 ++ src/lib/components/DailyCalendar.svelte | 68 + src/lib/components/Database.svelte | 1370 +++++++++++++++++++++ src/lib/components/Editor.svelte | 34 + src/lib/components/ExportDialog.svelte | 85 ++ src/lib/components/FindBar.svelte | 161 +++ src/lib/components/FolderPicker.svelte | 139 +++ src/lib/components/GraphView.svelte | 34 +- src/lib/components/HistoryDialog.svelte | 85 ++ src/lib/components/Icon.svelte | 13 + src/lib/components/IconPicker.svelte | 110 ++ src/lib/components/Outline.svelte | 103 ++ src/lib/components/PageView.svelte | 226 +++- src/lib/components/Palette.svelte | 170 +++ src/lib/components/PropCell.svelte | 192 +++ src/lib/components/SelectMenu.svelte | 127 ++ src/lib/components/SettingsDialog.svelte | 587 +++++++++ src/lib/components/Sidebar.svelte | 302 +++++ src/lib/components/SplitPane.svelte | 176 +++ src/lib/components/TagsDialog.svelte | 68 + src/lib/components/TasksDialog.svelte | 139 +++ src/lib/components/TemplateDialog.svelte | 127 ++ src/lib/components/TextMenu.svelte | 158 +++ src/lib/components/Toast.svelte | 11 + src/lib/components/Topbar.svelte | 66 +- src/lib/components/TrashDialog.svelte | 89 ++ src/lib/components/TreeNode.svelte | 133 ++ src/lib/components/Welcome.svelte | 18 + src/lib/components/WindowControls.svelte | 25 + src/routes/+layout.js | 3 + src/routes/+layout.svelte | 11 + src/routes/+page.svelte | 126 +- 39 files changed, 8744 insertions(+), 94 deletions(-) create mode 100644 src/lib/components/Backlinks.svelte create mode 100644 src/lib/components/Board.svelte create mode 100644 src/lib/components/CanvasView.svelte create mode 100644 src/lib/components/ClipDialog.svelte create mode 100644 src/lib/components/ContextMenu.svelte create mode 100644 src/lib/components/CoverPicker.svelte create mode 100644 src/lib/components/CsvImportDialog.svelte create mode 100644 src/lib/components/DailyCalendar.svelte create mode 100644 src/lib/components/Database.svelte create mode 100644 src/lib/components/Editor.svelte create mode 100644 src/lib/components/ExportDialog.svelte create mode 100644 src/lib/components/FindBar.svelte create mode 100644 src/lib/components/FolderPicker.svelte create mode 100644 src/lib/components/HistoryDialog.svelte create mode 100644 src/lib/components/Icon.svelte create mode 100644 src/lib/components/IconPicker.svelte create mode 100644 src/lib/components/Outline.svelte create mode 100644 src/lib/components/Palette.svelte create mode 100644 src/lib/components/PropCell.svelte create mode 100644 src/lib/components/SelectMenu.svelte create mode 100644 src/lib/components/SettingsDialog.svelte create mode 100644 src/lib/components/Sidebar.svelte create mode 100644 src/lib/components/SplitPane.svelte create mode 100644 src/lib/components/TagsDialog.svelte create mode 100644 src/lib/components/TasksDialog.svelte create mode 100644 src/lib/components/TemplateDialog.svelte create mode 100644 src/lib/components/TextMenu.svelte create mode 100644 src/lib/components/Toast.svelte create mode 100644 src/lib/components/TrashDialog.svelte create mode 100644 src/lib/components/TreeNode.svelte create mode 100644 src/lib/components/Welcome.svelte create mode 100644 src/lib/components/WindowControls.svelte create mode 100644 src/routes/+layout.js create mode 100644 src/routes/+layout.svelte 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}