init record-view

This commit is contained in:
boxi committed 2026-08-16 13:56:26 +02:00
1 parent 420956ed73
commit 99f5b5698f
4 files changed
+548 -11

No files matched your search

+32 -1
View File
@@ -96,7 +96,38 @@ schema:
- **`extends` is the only opt-in inheritance**, kept separate from the intrinsic universal
substrate above.
## The Markdown body projects to the standard free-text field
Standard types have a free-text property in their DAV standard: VTODO/VEVENT `DESCRIPTION`,
vCard `NOTE`. **The Markdown body is that field** — it projects to `DESCRIPTION`/`NOTE` as
**Markdown source text**, and no separate "description" field is introduced.
- Rationale: Markdown is human-readable by design (the core principle). A foreign CalDAV/
CardDAV client won't *render* it, but shows legible plain text — the deliberate
human-readable trade-off, not a broken state. Keeping "one body" also avoids fragmenting
the record into two texts to maintain; the body stays canonical, `DESCRIPTION` is its
projection.
- Round-trip: an external edit to `DESCRIPTION` comes back as plain text and becomes body
content — lossy-tolerant and acceptable.
- Consequence for the UI: this is why structured types render a **core block** in the body
(title/summary, dates, contact fields) *plus* the free-text body — the core block are the
base fields, the body is the standard description. (See [`ui-ux.md`](ui-ux.md).)
## Core vs. custom is a type-definition concept
Which fields are "core" (base-schema, guaranteed a projection and a body rendering) vs.
free/custom is decided in the **type definition** (`schema` bundle), *not* surfaced as a
per-record badge in the read view. It shows up only (a) when defining/editing a type, and
(b) subtly in edit behaviour (core fields can't be removed and have a fixed place). See
[`ui-ux.md`](ui-ux.md) for the record-view treatment.
Note that base schemas — **especially `contact` and `event`** — define **many optional
fields** (vCard/iCalendar have dozens), most of which are empty on any given record. Empty
optional fields are therefore **hidden in the view** (a UI rule; see [`ui-ux.md`](ui-ux.md)),
not stored as empty keys.
## Open
- The concrete **base field lists per built-in type** and their exact vCard/iCalendar
property mapping (the still-open "base schema per type" work).
property mapping (the still-open "base schema per type" work) — including the body →
`DESCRIPTION`/`NOTE` projection above.
+56 -10
View File
@@ -35,13 +35,45 @@ theme** — so tokens here are named/structured to map onto Compose (`ColorSchem
its first image (else a placeholder); a location/track a **mini-map**; a saved query a
distinct glyph.
- **Record view**: a **full drill-in** in its own tab. Inherits the parent folder's **background
image** and **accent colour**. Header bar in the accent; tools: ⓘ info/fields, link-panel
toggle, ≡ menu.
image** and **accent colour**. The **title lives only in the header bar** (not repeated in the
body). Header tools: ⓘ (Info/properties), link icon (Links), ≡ menu — these **switch the right
panel** (there is no separate in-panel tab strip; the header buttons are the single control).
- **Outgoing links** are marked **inline** in the body text (like HTML links).
- **Links panel** (right, collapsible): **List/Graph switch on top**, then two **independent**
Incoming/Outgoing toggles (both can be active). List → two labelled sections; Graph → one
graph with **colour-coded edges** (incoming = red, outgoing = green). The Graph renderer in
the collection switcher is the same idea at collection scope.
- **Links panel** (right): **List/Graph switch on top**, then two **independent** Incoming/
Outgoing toggles (both can be active). List → two labelled sections; Graph → one graph with
**colour-coded edges** (incoming = red, outgoing = green). The Graph renderer in the
collection switcher is the same idea at collection scope.
## Record view: properties & core blocks
Properties are **always frontmatter** — only the *default rendering* differs by type
(AnyType-style, deliberately unlike Obsidian's always-at-top frontmatter):
- **Notes / custom types** → properties are **not** shown at the top of the note. They live in
the right **Info panel**. The user can **reference a property inline** into the body, where it
renders as a **key/value property pill**.
- **Standard types (Task / Contact / Event)** → the type defines a **core block always rendered
in the body**, because usability *and* the Card/CalDAV projection depend on it. The rest of the
properties still live in the Info panel. Core blocks:
- **Task**: a checkbox (done state) + title, plus core property pills (due, priority, status).
- **Contact**: name in the header; a contact card — avatar, email/phone/mobile rows, address
with a **mini-map**.
- **Event**: on the left a date card + time range + all-day toggle; on the **right edge** the
location as a **mini-map with the place text beside it**.
- The **core block = the type's base-schema fields** (see
[`schema-and-types.md`](schema-and-types.md)); the Markdown body projects to the standard
free-text field (`DESCRIPTION`/`NOTE`).
- **"Core vs. custom" is not shown as a per-record badge** in the read view — it is a
type-definition concern, surfacing only when defining a type and subtly in edit behaviour.
- **The right panel is closed by default.** Opening a record shows no panel; the header ⓘ
(Info) / link (Links) buttons open it, and pressing the *active* button again closes it
(toggle). Only one of Info/Links is shown at a time.
- **System metadata is its own read-only section.** `created` + `modified` (system-managed,
not user-editable) render in a separate "System" section of the Info panel — visually
distinct from the editable properties, and never at the top of the note body.
- **Empty optional fields are hidden** — in both the core block and the Info panel. Base
schemas (esp. contact/event) have many optional fields; revealing/adding them is an
edit-mode concern.
## Colour system
@@ -126,7 +158,16 @@ Already exercised by the main-view wireframe — the reusable set to formalise i
- **Rail item** — nav/action row; states: default, hover, active.
- **Tab** — states: active (accent fill), idle; with close affordance.
- **Button** — primary (`+New`, accent fill); icon buttons (record header tools: idle / active).
- **Panel** (collapsible, right) — links panel; sections + toggles + graph.
- **Panel** (collapsible, right) — Info (properties) / Links; switched by the header icon
buttons, no in-panel tab strip.
- **Property pill** — one shared key/value pill for inline-referenced (note) *and* core (task)
properties; identical styling in both places.
- **Property row** (Info panel) — type icon + key + value; note variant has an "insert into note"
(reference) action.
- **Core block** — per-type body block (task checkbox row, contact card, event date/location);
the type's base-schema fields rendered inline.
- **Mini-map** — small map crop with a pin, used in location tiles, contact address, event
location.
- **Direction toggle** — independent on/off with a semantic colour dot (in=red, out=green).
- **Breadcrumb** — clickable ancestors + current (display face).
- **Map surface** — full-view + mini; must render own points/tracks even without basemap tiles;
@@ -134,9 +175,14 @@ Already exercised by the main-view wireframe — the reusable set to formalise i
## Open decisions (next screens will settle these)
- **Record editor internals** — the fields/frontmatter panel behind ⓘ; inline task checkboxes
(checkbox↔task notation); `[[` autocomplete affordance; embed (`![[…]]`) rendering. *Next up.*
- **Map full-view** — controls, region download / offline affordance, cluster/selection.
- **Record editor internals** — the *edit* mode (the record view above is read/structure):
`[[` autocomplete affordance, embed (`![[…]]`) rendering, editing property values in the
panel, adding a property, and how core-field editing behaves (not removable, fixed place).
The read-side property/core-block model is settled (see "Record view" above).
- **GIS data display (next up)** — how **locations** and **tracks** render: a location's core
block (coordinates + mini-map), a track's core block (summary/stats + shape), their Box tiles,
and the **Map full-view/renderer** (controls, region download / offline affordance,
cluster/selection, own-points-always-render).
- **New / capture flow** — quick capture entry, type picker, where a new record lands.
- **Cover-image source** — user-set per folder vs. derived (first image / a map for
location-heavy folders); what a location tile shows (always mini-map, or a photo if present).
+6
View File
@@ -16,6 +16,12 @@ not production code, no build step. Open a file directly in a browser.
Incoming/Outgoing toggles — List → two sections, Graph → colour-coded edges (incoming red,
outgoing green). Outgoing links are also marked inline in the body text.
- [`record-view.html`](record-view.html) — the record drill-in, with a type switcher
(Note / Task / Contact / Event) in the tab bar. Shows the property model: notes/custom types
keep properties in the right **Info panel** (optionally referenced inline as key/value pills);
structured types render an always-visible **core block** in the body (task checkbox, contact
card, event date + location map). Panel switches Info/Links via the header icon buttons.
## Colour system (as decided)
- **Chrome** stays neutral grey so per-folder accents never clash.
+454
View File
@@ -0,0 +1,454 @@
<meta charset="utf-8">
<title>Terrata — Record View</title>
<style>
:root{
--terra:#C85C3C;
--bg:#E8E7E3; --surface:#FFFFFF; --surface-2:#F4F3F0;
--text:#26251F; --muted:#78766F; --line:#E0DED8; --line-strong:#CDCAC2;
--chrome:#2B2A27; --chrome-text:#ECEAE4; --chrome-muted:#A6A39B;
--accent:#2F7A64; --edge-in:#C24A3B; --edge-out:#3E8E5E;
--shadow:0 1px 2px rgba(20,20,18,.06), 0 8px 22px rgba(20,20,18,.09);
--font-display:"Space Grotesk","Avenir Next","Segoe UI",system-ui,sans-serif;
--font-body:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
--r:12px; --r-sm:8px;
}
@media (prefers-color-scheme: dark){:root{
--bg:#1A1A18; --surface:#242320; --surface-2:#1F1E1C; --text:#ECEAE4; --muted:#95928A;
--line:#322F2B; --line-strong:#413D38; --chrome:#141312; --chrome-text:#ECEAE4; --chrome-muted:#8C8981;
--accent:#4E9E86; --edge-in:#DB6857; --edge-out:#57B07E;
--shadow:0 1px 2px rgba(0,0,0,.35), 0 10px 26px rgba(0,0,0,.4);
}}
:root[data-theme="light"]{--bg:#E8E7E3;--surface:#FFFFFF;--surface-2:#F4F3F0;--text:#26251F;--muted:#78766F;--line:#E0DED8;--line-strong:#CDCAC2;--chrome:#2B2A27;--chrome-text:#ECEAE4;--chrome-muted:#A6A39B;--accent:#2F7A64;--edge-in:#C24A3B;--edge-out:#3E8E5E;--shadow:0 1px 2px rgba(20,20,18,.06),0 8px 22px rgba(20,20,18,.09)}
:root[data-theme="dark"]{--bg:#1A1A18;--surface:#242320;--surface-2:#1F1E1C;--text:#ECEAE4;--muted:#95928A;--line:#322F2B;--line-strong:#413D38;--chrome:#141312;--chrome-text:#ECEAE4;--chrome-muted:#8C8981;--accent:#4E9E86;--edge-in:#DB6857;--edge-out:#57B07E;--shadow:0 1px 2px rgba(0,0,0,.35),0 10px 26px rgba(0,0,0,.4)}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--text);font-family:var(--font-body);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}
button{font-family:inherit;color:inherit;cursor:pointer}
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
.icon{width:18px;height:18px;flex:none}
svg{stroke-linecap:round;stroke-linejoin:round}
.app{display:flex;flex-direction:column;height:100vh;min-height:0}
.top{display:flex;align-items:center;gap:14px;padding:0 16px;height:52px;background:var(--chrome);
color:var(--chrome-text);border-bottom:4px solid var(--accent);transition:border-color .18s ease;flex:none}
.wordmark{font-family:var(--font-display);font-weight:600;font-size:19px;letter-spacing:.02em;padding-right:4px}
.wordmark .dot{color:var(--terra)}
.tabbar{display:flex;align-items:stretch;gap:3px;height:100%;padding-top:9px}
.tab{display:flex;align-items:center;gap:7px;padding:0 13px;border-radius:8px 8px 0 0;background:transparent;
color:var(--chrome-muted);border:0;font-size:13px}
.tab.active{background:var(--tabaccent,var(--accent));color:#fff;font-weight:600}
.globalsearch{margin-left:auto;display:flex;align-items:center;gap:8px;background:rgba(255,255,255,.08);
border:1px solid rgba(255,255,255,.12);padding:7px 12px;border-radius:999px;width:min(280px,26vw)}
.globalsearch input{background:transparent;border:0;outline:0;color:var(--chrome-text);width:100%;font-size:13px}
.globalsearch input::placeholder{color:var(--chrome-muted)}
.avatar{width:29px;height:29px;border-radius:50%;background:radial-gradient(circle at 30% 30%,#bbb,#555);border:2px solid var(--chrome-text);flex:none}
.scroll{flex:1;min-height:0;overflow:auto;padding:18px 22px 40px}
.reccard{max-width:1160px;margin:0 auto;background:var(--surface);border:1px solid var(--line);border-radius:var(--r);
box-shadow:var(--shadow);overflow:hidden;display:grid;grid-template-columns:1fr 320px}
.reccard.nopanel{grid-template-columns:1fr}
.rechead{grid-column:1/-1;display:flex;align-items:center;gap:12px;padding:12px 16px;background:var(--accent);color:#fff;transition:background-color .18s ease}
.rechead .rc{display:flex;align-items:center;gap:7px;font-family:var(--font-display);font-weight:600;font-size:15px}
.rechead .rc .type{opacity:.85;font-weight:500}
.rechead .rc .sep{opacity:.55}
.rechead .rc button{background:0;border:0;color:#fff;opacity:.9;padding:2px 4px;border-radius:6px;display:grid;place-items:center}
.rechead .rc button:hover{opacity:1;background:#ffffff2a}
.rechead .tools{margin-left:auto;display:flex;gap:6px}
.rechead .tools button{width:30px;height:30px;border-radius:50%;border:0;background:#ffffff2a;color:#fff;display:grid;place-items:center}
.rechead .tools button.on{background:#fff;color:var(--accent)}
.recbody{padding:22px 28px;min-width:0}
.recbody h1{font-family:var(--font-display);font-size:24px;margin:0 0 4px;font-weight:600;text-wrap:balance}
.recbody .sub{color:var(--muted);font-size:13px;margin-bottom:18px}
.recbody p{margin:0 0 14px;max-width:64ch}
.recbody a.wlink{color:var(--edge-out);text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:1.5px;cursor:pointer;font-weight:500}
.prose{margin-top:6px}
/* inline referenced property (key/value) */
/* one shared "property pill" for inline-referenced (note) and core (task) properties */
.kv{display:inline-flex;align-items:center;gap:7px;background:var(--surface-2);border:1px solid var(--line);
border-radius:999px;padding:3px 12px;font-size:12.5px;vertical-align:baseline}
.kv .k{color:var(--muted)}
.kv .v{font-weight:500}
/* ---- type core blocks (always shown for structured types) ---- */
.core{margin:0 0 20px;padding:0 0 18px;border-bottom:1px solid var(--line)}
/* Task */
.taskrow{display:flex;align-items:center;gap:14px}
.taskcheck{width:28px;height:28px;border:2px solid var(--accent);border-radius:8px;display:grid;place-items:center;background:transparent;flex:none}
.taskcheck svg{opacity:0;color:#fff;width:18px;height:18px}
.taskcheck.done{background:var(--accent)} .taskcheck.done svg{opacity:1}
.tasktitle{font-family:var(--font-display);font-size:22px;font-weight:600}
.taskcheck.done + .tasktitle{text-decoration:line-through;color:var(--muted)}
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
.chip{display:inline-flex;align-items:center;gap:7px;background:var(--surface-2);border:1px solid var(--line);
border-radius:999px;padding:5px 12px;font-size:12.5px}
.chip .k{color:var(--muted)}
.chip .badge{width:8px;height:8px;border-radius:50%;background:var(--accent)}
/* Contact */
.contact{display:flex;gap:20px;flex-wrap:wrap}
.cavatar{width:66px;height:66px;border-radius:50%;background:var(--accent);color:#fff;display:grid;place-items:center;
font-family:var(--font-display);font-weight:600;font-size:24px;flex:none}
.cinfo{display:flex;flex-direction:column;gap:9px;min-width:220px;flex:1}
.crow{display:flex;align-items:center;gap:11px;font-size:14px}
.crow .ci{width:30px;height:30px;border-radius:8px;background:var(--surface-2);border:1px solid var(--line);
display:grid;place-items:center;color:var(--muted);flex:none}
.crow .cl{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.05em;font-family:var(--font-display);width:64px;flex:none}
.addr{display:flex;gap:14px;align-items:stretch;margin-top:2px}
.addr .atext{display:flex;flex-direction:column;gap:2px;justify-content:center}
.minimap{width:150px;height:104px;border-radius:var(--r-sm);border:1px solid var(--line);position:relative;overflow:hidden;flex:none;
background:repeating-linear-gradient(0deg,var(--line) 0 1px,transparent 1px 22px),repeating-linear-gradient(90deg,var(--line) 0 1px,transparent 1px 22px),var(--surface-2)}
.minimap .pin{position:absolute;left:50%;top:48%;transform:translate(-50%,-50%);width:14px;height:14px;border-radius:50%;
background:var(--accent);box-shadow:0 0 0 5px color-mix(in srgb,var(--accent) 24%,transparent)}
/* Event */
.evgrid{display:flex;flex-wrap:wrap;gap:22px;align-items:center}
.evleft{display:flex;gap:22px;align-items:flex-start}
.evright{display:flex;gap:14px;align-items:center;margin-left:auto}
.datecard{width:74px;border:1px solid var(--line);border-radius:var(--r-sm);overflow:hidden;text-align:center;flex:none}
.datecard .m{background:var(--accent);color:#fff;font-size:11px;letter-spacing:.08em;text-transform:uppercase;padding:3px 0;font-family:var(--font-display)}
.datecard .d{font-size:30px;font-weight:700;padding:6px 0 2px;font-family:var(--font-display)}
.datecard .w{font-size:11px;color:var(--muted);padding-bottom:6px}
.evwhen{display:flex;flex-direction:column;gap:8px}
.evtime{font-size:16px;font-weight:600;font-family:var(--font-display)}
.evline{display:flex;align-items:center;gap:9px;color:var(--muted);font-size:13.5px}
.switchmini{display:inline-flex;align-items:center;gap:8px;font-size:13px;color:var(--muted)}
.toggle{width:34px;height:20px;border-radius:999px;background:var(--line-strong);position:relative;flex:none}
.toggle.on{background:var(--accent)}
.toggle .k{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#fff;transition:left .15s}
.toggle.on .k{left:16px}
/* ---- right panel (Info / Links) ---- */
.panel{border-left:1px solid var(--line);background:var(--surface-2);padding:14px 15px;min-width:0}
.ptabs{display:flex;gap:4px;margin-bottom:12px}
.ptabs button{flex:1;border:1px solid var(--line);background:var(--surface);color:var(--muted);padding:7px;border-radius:var(--r-sm);font-size:12.5px;font-weight:600;display:flex;align-items:center;justify-content:center;gap:6px}
.ptabs button.active{background:var(--accent);color:#fff;border-color:var(--accent)}
.panellabel{display:flex;align-items:center;margin:6px 0 9px}
.panellabel .t{font-family:var(--font-display);text-transform:uppercase;letter-spacing:.08em;font-size:10px;color:var(--muted);font-weight:600}
.prop{display:flex;align-items:flex-start;gap:9px;padding:8px 4px;border-bottom:1px solid var(--line)}
.prop:last-of-type{border-bottom:0}
.prop .pi{width:22px;height:22px;border-radius:6px;background:var(--surface);border:1px solid var(--line);display:grid;place-items:center;color:var(--muted);flex:none;margin-top:1px}
.prop .pmain{flex:1;min-width:0}
.prop .pk{display:flex;align-items:center;gap:7px;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-family:var(--font-display);font-weight:600}
.prop .pv{font-size:13.5px;margin-top:1px;word-break:break-word}
.prop .pv .tag{display:inline-block;background:var(--surface);border:1px solid var(--line);border-radius:999px;padding:0 8px;font-size:12px;margin:2px 4px 0 0}
.kernbadge{font-size:9px;background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--accent);border-radius:4px;padding:1px 5px;letter-spacing:.04em}
.refbadge{font-size:9px;background:var(--surface);border:1px solid var(--line);color:var(--muted);border-radius:4px;padding:1px 5px;letter-spacing:.04em}
.insertbtn{border:0;background:transparent;color:var(--muted);opacity:.55;padding:2px;border-radius:5px;flex:none}
.prop:hover .insertbtn{opacity:1}
.insertbtn:hover{background:var(--surface);color:var(--accent)}
.sysprop{opacity:.9}
.sysprop .pi{background:transparent;border-color:transparent}
.sysprop .pv{color:var(--muted)}
.addprop{margin-top:12px;display:flex;align-items:center;gap:8px;border:1px dashed var(--line-strong);background:transparent;
color:var(--muted);width:100%;justify-content:center;padding:9px;border-radius:var(--r-sm);font-size:13px;font-weight:600}
.addprop:hover{color:var(--accent);border-color:var(--accent)}
.panelnote{margin-top:14px;font-size:11.5px;color:var(--muted);line-height:1.45;border-top:1px solid var(--line);padding-top:10px}
.linkrow{display:flex;align-items:center;gap:9px;padding:8px 10px;background:var(--surface);border:1px solid var(--line);border-radius:var(--r-sm);margin-bottom:6px;font-size:13px}
.linkrow .ficon{width:16px;height:16px;color:var(--muted)}
.seclabel{display:flex;align-items:center;gap:7px;margin:10px 0 7px}
.seclabel .d{width:9px;height:9px;border-radius:50%}
.seclabel.in .d{background:var(--edge-in)} .seclabel.out .d{background:var(--edge-out)}
.seclabel .t{font-family:var(--font-display);text-transform:uppercase;letter-spacing:.08em;font-size:10px;color:var(--muted);font-weight:600}
.seclabel .n{margin-left:auto;font-size:11px;color:var(--muted);background:var(--surface);border:1px solid var(--line);border-radius:999px;padding:1px 7px}
.switch2{display:flex;background:var(--surface);border:1px solid var(--line);border-radius:999px;padding:3px;margin-bottom:10px}
.switch2 button{flex:1;border:0;background:transparent;color:var(--muted);padding:6px;border-radius:999px;font-size:12.5px;font-weight:600}
.switch2 button.active{background:var(--accent);color:#fff}
.dirtoggles{display:flex;gap:6px;margin-bottom:12px}
.tg{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--line);background:var(--surface);color:var(--muted);border-radius:var(--r-sm);padding:6px;font-size:12px;font-weight:600}
.tg .d{width:9px;height:9px;border-radius:50%}
.tg.in .d{background:var(--edge-in)} .tg.out .d{background:var(--edge-out)}
.tg.on{color:var(--text);border-color:var(--line-strong)}
.tg.on.in{background:color-mix(in srgb,var(--edge-in) 12%,transparent)}
.tg.on.out{background:color-mix(in srgb,var(--edge-out) 12%,transparent)}
.tg.off{opacity:.45}
.emptyp{color:var(--muted);font-size:12.5px;text-align:center;padding:20px 0}
.graphmini{height:210px;border:1px solid var(--line);border-radius:var(--r-sm);background:var(--surface);position:relative}
.graphmini svg{position:absolute;inset:0;width:100%;height:100%}
.graphmini .node{position:absolute;border-radius:50%;transform:translate(-50%,-50%)}
.graphmini .node.c{width:16px;height:16px;background:var(--accent)}
.graphmini .node.n{width:11px;height:11px;background:var(--muted)}
.graphmini .lbl{position:absolute;transform:translate(-50%,6px);font-size:9.5px;color:var(--muted);white-space:nowrap}
.glegend{display:flex;gap:14px;justify-content:center;margin-top:8px;font-size:11px;color:var(--muted)}
.glegend span{display:inline-flex;align-items:center;gap:5px}
.glegend i{width:14px;height:0;border-top:2px solid}
.demohint{max-width:1160px;margin:10px auto 0;font-size:12px;color:var(--muted);text-align:center}
.themebtn{position:fixed;right:14px;bottom:14px;width:38px;height:38px;border-radius:50%;background:var(--surface);border:1px solid var(--line);box-shadow:var(--shadow);display:grid;place-items:center;color:var(--text)}
</style>
<div class="app" id="app">
<header class="top">
<span class="wordmark">terrata<span class="dot">.</span></span>
<nav class="tabbar" id="tabbar"></nav>
<div class="globalsearch">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg>
<input placeholder="Global Search…" aria-label="Global search">
</div>
<div class="avatar"></div>
</header>
<div class="scroll">
<div class="reccard" id="reccard">
<div class="rechead">
<div class="rc">
<button title="Zurück"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 11l9-8 9 8"/><path d="M5 10v10h14V10"/></svg></button>
<span class="sep">/</span><span class="type" id="recType">Notiz</span>
<span class="sep">/</span><span id="recCrumb">Titel</span>
</div>
<div class="tools">
<button id="infoBtn" title="Info / Properties" onclick="clickPanel('info')"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/></svg></button>
<button id="linkBtn" title="Links" onclick="clickPanel('links')"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 15 15 9"/><path d="M11 7h4a3 3 0 0 1 0 6h-1"/><path d="M13 17H9a3 3 0 0 1 0-6h1"/></svg></button>
<button title="Bearbeiten"><svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg></button>
</div>
</div>
<div class="recbody" id="recbody"></div>
<aside class="panel" id="panel">
<div id="panelInfo"></div>
<div id="panelLinks" style="display:none">
<div class="switch2">
<button class="active" data-pm="list" onclick="setPanelMode('list')">Liste</button>
<button data-pm="graph" onclick="setPanelMode('graph')">Graph</button>
</div>
<div class="dirtoggles">
<button class="tg in on" data-dir="in" onclick="toggleDir('in')"><span class="d"></span>Eingehend</button>
<button class="tg out on" data-dir="out" onclick="toggleDir('out')"><span class="d"></span>Ausgehend</button>
</div>
<div id="panelList"></div>
<div id="panelGraph" style="display:none">
<div class="graphmini" id="graphmini"></div>
<div class="glegend">
<span><i style="border-color:var(--edge-in)"></i>eingehend</span>
<span><i style="border-color:var(--edge-out)"></i>ausgehend</span>
</div>
</div>
</div>
</aside>
</div>
<p class="demohint">Demo: oben den Typ wechseln. Notiz/eigene Typen → Properties nur im Info-Panel (optional inline referenziert). Task/Kontakt/Termin → Kern-Block immer im Body.</p>
</div>
</div>
<button class="themebtn" id="themebtn" title="Theme wechseln"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 3v18a9 9 0 0 0 0-18Z" fill="currentColor" stroke="none"/></svg></button>
<script>
const PI = {
text:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7V5h16v2M9 19h6M12 5v14"/></svg>',
select:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>',
date:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="17" rx="2"/><path d="M3 9h18M8 2v4M16 2v4"/></svg>',
multi:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 12H10M20 6H10M20 18H10M4 6h.01M4 12h.01M4 18h.01"/></svg>',
check:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="4"/><path d="m8 12 3 3 5-6"/></svg>',
email:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg>',
phone:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h4l2 5-3 2a12 12 0 0 0 6 6l2-3 5 2v4a2 2 0 0 1-2 2A17 17 0 0 1 2 6a2 2 0 0 1 2-2Z"/></svg>',
address:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 21s7-6.3 7-11a7 7 0 1 0-14 0c0 4.7 7 11 7 11Z"/><circle cx="12" cy="10" r="2.5"/></svg>',
relation:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 15 15 9"/><path d="M11 7h4a3 3 0 0 1 0 6h-1"/><path d="M13 17H9a3 3 0 0 1 0-6h1"/></svg>',
number:'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 4v16M18 4v16M4 9h16M4 15h16"/></svg>',
};
const RECORDS = {
note:{tab:"Notiz",icon:'<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v17H6z"/><path d="M9 8h6M9 12h6M9 16h4"/></svg>',
accent:"#2F7A64", type:"Notiz", title:"Projektidee Terrata",
core:null,
body:'<div class="prose" id="prose"><p>Local-first PKM mit GIS. Der aktuelle <span class="kv"><span class="k">Status</span><span class="v">In Arbeit</span></span> passt gut zum Plan. '
+'Siehe auch <a class="wlink">[[Architektur]]</a>.</p><p>Nächster Schritt: Record-View finalisieren.</p></div>',
props:[
{k:"Status",t:"select",v:"In Arbeit",ref:true},
{k:"Priorität",t:"select",v:"Mittel"},
{k:"Tags",t:"multi",v:["pkm","kotlin","gis"]},
{k:"Erstellt",t:"date",v:"2026-07-26"},
]},
task:{tab:"Task",icon:'<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="m8 12 3 3 5-6"/></svg>',
accent:"#E08A2B", type:"Task", title:"Route planen",
core:'<div class="core">'
+'<div class="taskrow"><button class="taskcheck" id="taskcheck" onclick="toggleTask()"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><path d="m5 12 4.5 4.5L19 6"/></svg></button><span class="tasktitle" id="tasktitle">Route planen</span></div>'
+'<div class="chips"><span class="chip">'+PI.date+'<span class="k">Fällig</span> 02.08.2026</span>'
+'<span class="chip"><span class="badge"></span><span class="k">Priorität</span> Hoch</span>'
+'<span class="chip"><span class="k">Status</span> Offen</span></div></div>',
body:'<div class="prose"><p>Alternativroute über den Grat prüfen, Wetterfenster am Wochenende beachten. Verknüpft mit <a class="wlink">[[Wanderung]]</a>.</p></div>',
props:[
{k:"Erledigt",t:"check",v:"Nein",kern:true},
{k:"Fällig",t:"date",v:"2026-08-02",kern:true},
{k:"Priorität",t:"select",v:"Hoch",kern:true},
{k:"Status",t:"select",v:"Offen",kern:true},
{k:"Projekt",t:"relation",v:"Terrata"},
]},
contact:{tab:"Kontakt",icon:'<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>',
accent:"#3E7CB1", type:"Kontakt", title:"Anna Muster",
core:'<div class="core"><div class="contact"><div class="cavatar">AM</div>'
+'<div class="cinfo">'
+'<div class="crow"><span class="ci">'+PI.email+'</span><span class="cl">E-Mail</span><span>anna.muster@example.com</span></div>'
+'<div class="crow"><span class="ci">'+PI.phone+'</span><span class="cl">Tel</span><span>+41 44 123 45 67</span></div>'
+'<div class="crow"><span class="ci">'+PI.phone+'</span><span class="cl">Mobil</span><span>+41 79 987 65 43</span></div>'
+'<div class="addr"><div class="minimap"><span class="pin"></span></div><div class="atext"><span class="cl" style="width:auto">Adresse</span><span>Bahnhofstrasse 1</span><span>8001 Zürich</span></div></div>'
+'</div></div></div>',
body:'<div class="prose"><p>Kennengelernt an der KMP-Konferenz. Interessiert an local-first Tooling.</p></div>',
props:[
{k:"Name",t:"text",v:"Anna Muster",kern:true},
{k:"E-Mail",t:"email",v:"anna.muster@example.com",kern:true},
{k:"Telefon",t:"phone",v:"+41 44 123 45 67",kern:true},
{k:"Mobil",t:"phone",v:"+41 79 987 65 43",kern:true},
{k:"Adresse",t:"address",v:"Bahnhofstrasse 1, 8001 Zürich",kern:true},
{k:"Fax",t:"phone",v:"",kern:true},
{k:"Organisation",t:"text",v:"Ergon"},
]},
event:{tab:"Termin",icon:'<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="17" rx="2"/><path d="M3 9h18M8 2v4M16 2v4"/></svg>',
accent:"#7A5AA6", type:"Termin", title:"Team Sync",
core:'<div class="core"><div class="evgrid">'
+'<div class="evleft">'
+'<div class="datecard"><div class="m">Aug</div><div class="d">05</div><div class="w">Mi</div></div>'
+'<div class="evwhen"><div class="evtime">09:00 – 10:00</div>'
+'<div class="switchmini"><span class="toggle"><span class="k"></span></span> Ganztägig</div></div>'
+'</div>'
+'<div class="evright"><div class="atext"><span class="cl" style="width:auto">Ort</span><span>Ergon</span><span>Zürich</span></div>'
+'<div class="minimap" style="width:170px;height:104px"><span class="pin"></span></div></div>'
+'</div></div>',
body:'<div class="prose"><p>Agenda: Sync-Adapter, iOS-Spike, Record-View. Teilnehmer: Anna, Boxi.</p></div>',
props:[
{k:"Beginn",t:"date",v:"2026-08-05 09:00",kern:true},
{k:"Ende",t:"date",v:"2026-08-05 10:00",kern:true},
{k:"Ganztägig",t:"check",v:"Nein",kern:true},
{k:"Ort",t:"address",v:"Ergon, Zürich",kern:true},
{k:"Teilnehmer",t:"multi",v:["Anna","Boxi"]},
]},
};
// read-only system metadata (present on every bundle; behaves unlike editable properties)
const SYS={note:{c:"26.07.2026",m:"29.07.2026"},task:{c:"20.07.2026",m:"28.07.2026"},
contact:{c:"12.05.2026",m:"03.07.2026"},event:{c:"25.07.2026",m:"25.07.2026"}};
const CLOCK='<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 8v4l3 2"/></svg>';
function isEmpty(v){return v==null||v===""||(Array.isArray(v)&&v.length===0);}
let curType="note";
function renderTabs(){
const tb=document.getElementById("tabbar"); tb.innerHTML="";
Object.keys(RECORDS).forEach(key=>{
const r=RECORDS[key];
const b=document.createElement("button");
b.className="tab"+(key===curType?" active":"");
b.style.setProperty("--tabaccent",r.accent);
b.innerHTML=r.icon+r.tab;
b.onclick=()=>{curType=key;render();};
tb.appendChild(b);
});
}
function propValue(p){
if(p.t==="multi") return p.v.map(x=>'<span class="tag">'+x+'</span>').join("");
return p.v;
}
// ---- Links panel (Liste / Graph, independent direction toggles) ----
const LINKIC={
note:'<svg class="ficon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2h9l3 3v17H6z"/><path d="M9 8h6M9 12h6"/></svg>',
query:'<svg class="ficon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18l-7 8v6l-4-2v-4z"/></svg>',
location:'<svg class="ficon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 21s7-6.3 7-11a7 7 0 1 0-14 0c0 4.7 7 11 7 11Z"/><circle cx="12" cy="10" r="2.5"/></svg>',
task:'<svg class="ficon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="m8 12 3 3 5-6"/></svg>',
};
const INLINKS=[["note","Reiseplanung"],["note","Journal 2026-07"],["query","Alle offenen Tasks"]];
const OUTLINKS=[["location","Aussichtspunkt"],["note","Wanderung"]];
const dirs={in:true,out:true}; let panelMode="list";
function linkRows(data){return data.map(d=>'<div class="linkrow">'+(LINKIC[d[0]]||LINKIC.note)+'<span>'+d[1]+'</span></div>').join("");}
function renderLinks(){
if(panelMode==="list"){
let h="";
if(dirs.in) h+='<div class="seclabel in"><span class="d"></span><span class="t">Eingehend</span><span class="n">'+INLINKS.length+'</span></div>'+linkRows(INLINKS);
if(dirs.out) h+='<div class="seclabel out"><span class="d"></span><span class="t">Ausgehend</span><span class="n">'+OUTLINKS.length+'</span></div>'+linkRows(OUTLINKS);
if(!dirs.in && !dirs.out) h='<div class="emptyp">Kein Link-Typ aktiv.<br>Eingehend oder Ausgehend einschalten.</div>';
document.getElementById("panelList").innerHTML=h;
} else { renderGraph(); }
}
function renderGraph(){
const gm=document.getElementById("graphmini"); const cx=50,cy=50;
const inN=[[24,20],[80,24],[20,52]], outN=[[74,72],[36,84]];
let svg='<svg>';
if(dirs.in) inN.forEach(n=>svg+='<line x1="'+cx+'%" y1="'+cy+'%" x2="'+n[0]+'%" y2="'+n[1]+'%" stroke="var(--edge-in)" stroke-width="1.8"/>');
if(dirs.out) outN.forEach(n=>svg+='<line x1="'+cx+'%" y1="'+cy+'%" x2="'+n[0]+'%" y2="'+n[1]+'%" stroke="var(--edge-out)" stroke-width="1.8"/>');
svg+='</svg>';
let nodes='<span class="node c" style="left:'+cx+'%;top:'+cy+'%"></span><span class="lbl" style="left:'+cx+'%;top:'+cy+'%">'+RECORDS[curType].title+'</span>';
if(dirs.in) inN.forEach(n=>nodes+='<span class="node n" style="left:'+n[0]+'%;top:'+n[1]+'%"></span>');
if(dirs.out) outN.forEach(n=>nodes+='<span class="node n" style="left:'+n[0]+'%;top:'+n[1]+'%"></span>');
gm.innerHTML=svg+nodes;
}
function setPanelMode(m){
panelMode=m;
document.querySelectorAll(".switch2 button").forEach(b=>b.classList.toggle("active",b.dataset.pm===m));
document.getElementById("panelList").style.display=m==="list"?"":"none";
document.getElementById("panelGraph").style.display=m==="graph"?"":"none";
renderLinks();
}
function toggleDir(d){
dirs[d]=!dirs[d];
const btn=document.querySelector('.tg[data-dir="'+d+'"]');
btn.classList.toggle("on",dirs[d]); btn.classList.toggle("off",!dirs[d]);
renderLinks();
}
function render(){
const r=RECORDS[curType];
panelOpen=false; // panel stays hidden when opening a record
document.getElementById("app").style.setProperty("--accent",r.accent);
document.getElementById("recType").textContent=r.type;
document.getElementById("recCrumb").textContent=r.title;
document.getElementById("recbody").innerHTML=(r.core||"")+r.body;
// Info panel: editable properties (empty ones are hidden)
let h='<div class="panellabel"><span class="t">Properties</span></div>';
r.props.forEach((p,i)=>{
if(isEmpty(p.v)) return; // no empty fields in the panel
h+='<div class="prop"><span class="pi">'+(PI[p.t]||PI.text)+'</span><div class="pmain">'
+'<div class="pk">'+p.k+'</div><div class="pv">'+propValue(p)+'</div></div>'
+(curType==="note"?'<button class="insertbtn" title="In Notiz referenzieren" onclick="insertProp('+i+')"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 10 4 15l5 5"/><path d="M20 4v7a4 4 0 0 1-4 4H4"/></svg></button>':'')
+'</div>';
});
h+='<button class="addprop"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg> Eigenschaft hinzufügen</button>';
// System metadata — a separate, read-only section (not manually editable)
const s=SYS[curType];
h+='<div class="panellabel" style="margin-top:18px"><span class="t">System</span></div>';
h+='<div class="prop sysprop"><span class="pi">'+CLOCK+'</span><div class="pmain"><div class="pk">Erstellt</div><div class="pv">'+s.c+'</div></div></div>';
h+='<div class="prop sysprop"><span class="pi">'+CLOCK+'</span><div class="pmain"><div class="pk">Zuletzt bearbeitet</div><div class="pv">'+s.m+'</div></div></div>';
h+='<div class="panelnote">'+(curType==="note"
? 'Eigene/simple Typen: Properties leben hier. Über das Einfügen-Symbol referenzierst du eine Property inline in den Text (dort als Key/Value dargestellt).'
: 'Kern-Properties dieses Typs werden immer im Body dargestellt. Leere optionale Felder werden ausgeblendet.')+'</div>';
document.getElementById("panelInfo").innerHTML=h;
renderLinks();
applyPanel();
renderTabs();
}
function insertProp(i){
const p=RECORDS.note.props[i];
const prose=document.getElementById("prose");
if(!prose) return;
const span=document.createElement("p");
span.innerHTML='Referenziert: <span class="kv"><span class="k">'+p.k+'</span><span class="v">'+(p.t==="multi"?p.v.join(", "):p.v)+'</span></span>';
prose.appendChild(span);
p.ref=true; render();
}
function toggleTask(){
const c=document.getElementById("taskcheck");
if(c) c.classList.toggle("done");
}
let panelOpen=false, panelTab="info";
function clickPanel(t){
if(panelOpen && panelTab===t) panelOpen=false; // press active button again → hide
else { panelOpen=true; panelTab=t; }
applyPanel();
}
function applyPanel(){
document.getElementById("reccard").classList.toggle("nopanel",!panelOpen);
document.getElementById("panel").style.display=panelOpen?"":"none";
document.getElementById("infoBtn").classList.toggle("on",panelOpen&&panelTab==="info");
document.getElementById("linkBtn").classList.toggle("on",panelOpen&&panelTab==="links");
document.getElementById("panelInfo").style.display=panelTab==="info"?"":"none";
document.getElementById("panelLinks").style.display=panelTab==="links"?"":"none";
}
document.getElementById("themebtn").onclick=()=>{
const r=document.documentElement;
const cur=r.getAttribute("data-theme")||(matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");
r.setAttribute("data-theme",cur==="dark"?"light":"dark");
};
render();
</script>
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.