init record-view
This commit is contained in:
1 parent
420956ed73
commit
99f5b5698f
4 files changed
+548
-11
No files matched your search
@@ -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
@@ -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).
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||