Merge remote-tracking branch 'origin/main'
# Conflicts: # CLAUDE.md # docs/design/geo.md # docs/design/schema-and-types.md
This commit is contained in:
commit
3b2c499488
7 files changed
+1582
-35
No files matched your search
@@ -106,8 +106,18 @@ the areas they cover; keep this file as the high-level index.
|
||||
date/time, and a worked YAML→vCard/ics example per type. Base fields are **append-only** from
|
||||
here on. Pitfall: the substrate's `title` is vCard `FN`, *not* vCard `TITLE` (that is
|
||||
`job_title`).
|
||||
- [`docs/design/field-types.md`](docs/design/field-types.md) — the **field-type catalogue**, split in
|
||||
**two**: the **palette** (13 types a *user* can add — deliberately simple, because a custom field
|
||||
projects to `X-TERRATA-*` and never into a standard property, so it needs no vCard labels) vs. the
|
||||
**base-schema shapes** (`labeled` multi-values, `recurrence`/`reminder`/`attendee`, codecs — only
|
||||
the built-in types, driven by vCard/iCalendar). Plus: a field is a *type plus modifiers*
|
||||
(`multiple` instead of a MultiSelect type), the frontmatter form per type (incl. the YAML pitfalls:
|
||||
quoted `"[[id]]"`, YAML-1.2 reading, frontmatter round-trip with comments/key order), the
|
||||
**click-to-edit rule** (scalar or flat list = no mode ⇒ *every* user field is click-to-edit) with
|
||||
the rendering per surface, the `raw` fallback, and the index mapping per storage kind. Wireframe:
|
||||
[`wireframes/field-types.html`](docs/design/wireframes/field-types.html).
|
||||
- [`docs/design/geo.md`](docs/design/geo.md) — geospatial storage: coordinates as named
|
||||
frontmatter fields, addresses as vCard ADR + a derived (cached) coordinate, and tracks
|
||||
frontmatter fields, addresses as vCard ADR + a **nested** (cached) coordinate, and tracks
|
||||
as an open-format file (GPX/GeoJSON) in the bundle with only a summary + bbox in
|
||||
frontmatter for the R-Tree.
|
||||
- [`docs/design/ui-ux.md`](docs/design/ui-ux.md) — the UI/UX & design system: note-centric IA
|
||||
@@ -141,6 +151,10 @@ hub — every device is a full replica. **Desktop (macOS) first**, iOS the prior
|
||||
target; Windows/Linux/Android deliberately not foreclosed. Details and rationale in
|
||||
[`docs/design/tech-stack.md`](docs/design/tech-stack.md).
|
||||
|
||||
Next design blocks before/alongside coding: the **sync engine**, the **per-type base
|
||||
schema** (YAML ↔ vCard/iCalendar — the *field-type vocabulary* it is written in is now fixed in
|
||||
[`field-types.md`](docs/design/field-types.md)), and the concrete **index** tables.
|
||||
|
||||
The **per-type base schema** (YAML ↔ vCard/iCalendar) is settled as of 2026-08-19 — see
|
||||
[`docs/design/base-schemas.md`](docs/design/base-schemas.md); base fields are append-only from
|
||||
here on. Next design blocks before/alongside coding: the **sync engine** and the concrete
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
# Design: Field types (attributes)
|
||||
|
||||
Status: **proposed (early design)** — date 2026-08-19. The catalogue of field types: which types
|
||||
a *user* can build with, which shapes only the *built-in* types use, how each is stored in the
|
||||
frontmatter, how each is rendered and edited on every surface, and how each is indexed.
|
||||
|
||||
Read with [`schema-and-types.md`](schema-and-types.md) (where a type *definition* lives: the
|
||||
`schema` bundle, `custom:` namespace, `extends`), [`ui-ux.md`](ui-ux.md) (the surfaces: pill /
|
||||
panel row / core block / field-edit state), [`geo.md`](geo.md) (the geo storage rules this doc
|
||||
only references) and [`queries.md`](queries.md) (what the index has to support).
|
||||
The visual counterpart is [`wireframes/field-types.html`](wireframes/field-types.html).
|
||||
|
||||
## Two catalogues, not one
|
||||
|
||||
The distinction that structures everything below:
|
||||
|
||||
| | **The palette** | **Base-schema shapes** |
|
||||
|---|---|---|
|
||||
| Who declares it | the **user**, in a `schema` bundle / "+ Eigenschaft" | **we**, in code, per built-in type |
|
||||
| Driven by | what a person wants to record | vCard / iCalendar |
|
||||
| Complexity | **deliberately simple** — a value, maybe several | whatever the standard demands |
|
||||
| Projection | `X-TERRATA-*` (never a standard property) | the standard property itself |
|
||||
| Editing | **always click-to-edit**, no mode | mostly the field-edit state |
|
||||
|
||||
**Why the palette can be simpler.** A custom field never projects into a standard vCard/iCalendar
|
||||
property — it becomes `X-TERRATA-<NAME>` ([`schema-and-types.md`](schema-and-types.md)). Everything
|
||||
that makes the base fields awkward exists *because* of the standard:
|
||||
|
||||
- **Labels** (`{label, value}`) exist because vCard has `TYPE` parameters — `TEL;TYPE=work`. An
|
||||
`X-TERRATA-*` property has no `TYPE` vocabulary, so a user's `email` field is a **plain value**,
|
||||
not a labeled row. Same for the drag-to-reorder = `PREF` priority: nothing consumes it.
|
||||
- **`RRULE` / `VALARM` / `ATTENDEE`** are only meaningful to a CalDAV client. A user-defined
|
||||
"recurrence" that no calendar reads would be a text field pretending to be a feature.
|
||||
- **Structured `ADR` parts, the `PRIORITY` 0–9 codec, `TZID`** — all of them are round-trip
|
||||
fidelity, not user expressiveness.
|
||||
|
||||
So the palette is close to the original proposal, and the complicated shapes stay where they are
|
||||
needed: inside the six built-in types.
|
||||
|
||||
**Consequence worth stating:** since no palette type has structure beyond "a value or a list of
|
||||
values", **every user-defined field is editable in place, without a mode**. The field-edit state
|
||||
(the header pencil) therefore exists almost exclusively for the *built-in* core blocks — which is
|
||||
exactly what [`ui-ux.md`](ui-ux.md) already assumed but did not derive.
|
||||
|
||||
## Two axes: type + modifiers
|
||||
|
||||
A field is a **type plus modifiers**, never a type per combination:
|
||||
|
||||
```yaml
|
||||
fields:
|
||||
genres: { type: select, options: [Drama, SciFi], multiple: true }
|
||||
homepage: { type: url }
|
||||
synopsis: { type: text, multiline: true }
|
||||
```
|
||||
|
||||
"MultiSelect" is therefore not a type — it is `select` + `multiple`. The reason is that
|
||||
multiplicity is needed on *almost every* type (several links, several tags, several files), so a
|
||||
type-per-cardinality list would double and then double again while saying nothing new.
|
||||
|
||||
| Modifier | User-visible | Applies to | Meaning |
|
||||
|---|---|---|---|
|
||||
| `multiple` | ✅ | all palette types | YAML sequence; order preserved for round-trip, **not** semantic |
|
||||
| `options` / `open` | ✅ | `select` | the option set (each may carry a colour); `open` = free values allowed (a tag field) |
|
||||
| `to` | ✅ | `link` | restrict the target `type` → a labeled edge in the graph |
|
||||
| `accept` | ✅ | `file` | mime/extension filter (`accept: image/*` is how a cover image is declared) |
|
||||
| `unit` | ✅ | `number` | display suffix only; the **value is always in the canonical unit** |
|
||||
| `min` / `max` / `precision` | ✅ | `number`, `date` | advisory range; drives `display: rating` / `progress` |
|
||||
| `multiline` | ✅ | `text` | a paragraph field instead of a single line |
|
||||
| `display` | ✅ | `number` | `plain` (default) · `rating` · `progress` |
|
||||
| `default` / `required` | ✅ | all | pre-filled value; `required` is **advisory** (flagged, never rejected) |
|
||||
| `labeled` | — | base schema only | sequence of `{label, value}`; **order = priority** (replaces vCard `PREF`) |
|
||||
| `readonly` | — | base schema only | system-managed (`id`, `created`, `modified`, `creator`) |
|
||||
|
||||
Modifiers are **display and validation metadata**; they never change where the value lives.
|
||||
|
||||
## Storage rules (frontmatter)
|
||||
|
||||
1. **Scalars stay scalars.** An object is used only when the value genuinely has parts
|
||||
(`geo`, `address`) — and then always with **named keys, never positional** (the `[lon, lat]` vs
|
||||
`lat,lon` trap, see [`geo.md`](geo.md)).
|
||||
2. **Absent = unset.** An optional field without a value has **no key at all**; `""`, `[]` and
|
||||
`null` are never written. This is what makes "empty optional fields are hidden" a storage fact
|
||||
rather than a UI trick.
|
||||
3. **Follow the standard's own canonical form** where one exists — ISO-8601 for dates and
|
||||
durations, `RRULE` verbatim, vCard `ADR` component names, `#rrggbb`. Re-encoding a standard
|
||||
string into bespoke YAML is the most reliable way to lose it.
|
||||
4. **Values are readable without the schema.** A person opening the file must recognize a date as
|
||||
a date and a link as a link. This is why a `select` stores its *value*, not an opaque option id.
|
||||
5. **Cached values live in the file — inside the object of the thing they were computed from.**
|
||||
Storing a resolved value in the record (rather than only in the index) is deliberate: the index
|
||||
is **per-device and not synced**, so an index-only geocode means *every* device resolves *every*
|
||||
address again. With a **rate-limited free geocoding service** that is not a cosmetic difference
|
||||
— it is the difference between one request per address per vault and one per address **per
|
||||
device**. The file is the only place a resolved value can be shared.
|
||||
|
||||
The concern this raises — "which values did a person enter, which did we compute?" — is answered
|
||||
**structurally instead of by a flag**: a cached value is **nested in the object of its source**,
|
||||
never floating at the top level.
|
||||
|
||||
| Source in the file | Cached beside it |
|
||||
|---|---|
|
||||
| `address.street` / `postcode` / `locality` … (typed) | `address.geo` (geocoded) |
|
||||
| `track.file` (the GPX) | `track.bbox`, `distance_m`, `ascent_m`, … (computed from it) |
|
||||
|
||||
So the object *is* the marker: everything under `address:` except `geo` is typed, everything
|
||||
under `track:` except `file` is computed. It also makes the pair one **rendering unit** (an
|
||||
address with its map, a track with its stats), which is how the UI treats it anyway.
|
||||
|
||||
Rules that follow: resolution happens **only on an explicit user action**, never while rendering;
|
||||
a changed source only *offers* re-resolving; and a value the user corrected by hand
|
||||
(`geo: { …, manual: true }`) is never silently overwritten. `manual` is written only when it
|
||||
applies — its absence means "as resolved".
|
||||
|
||||
### YAML pitfalls the writer must handle
|
||||
|
||||
- `[[<id>]]` **unquoted is a nested YAML sequence.** Link values are therefore **always quoted**:
|
||||
`project: "[[a1b2]]"`.
|
||||
- **Quote identifier-ish strings**: postcodes (`"8001"`), phone numbers (`"+41 …"`) — otherwise
|
||||
leading zeros and `+` are lost or reinterpreted.
|
||||
- **Read with the YAML 1.2 core schema.** A YAML 1.1 parser coerces `2026-08-19` into a timestamp
|
||||
object and `yes`/`no`/`on`/`off` into booleans. Terrata writes dates as **unquoted ISO-8601**
|
||||
(readable, diffable) and booleans only as `true`/`false`, and must not let the parser retype them.
|
||||
- **Frontmatter round-trip.** The `parse → serialize` identity rule from
|
||||
[`notes-and-records.md`](notes-and-records.md) applies to the frontmatter too: **key order,
|
||||
unknown keys, value shape and YAML comments must survive** a record Terrata writes. A plain
|
||||
data-class (de)serializer loses comments and key order, so the frontmatter layer has to be a
|
||||
**node-level model** with the typed view on top — a real implementation constraint, not a nicety.
|
||||
|
||||
## The palette — what a user can add
|
||||
|
||||
13 types. This list *is* the "+ Eigenschaft" menu and the type editor's field-type picker.
|
||||
|
||||
### A · Scalars
|
||||
|
||||
| Type | UI name | Frontmatter | Notes |
|
||||
|---|---|---|---|
|
||||
| `text` | Text | `synopsis: Zwei Tage, ein Grat` | `multiline: true` → a YAML block scalar (`\|`) |
|
||||
| `number` | Zahl | `alt: 1322` | bare number; `unit`/`precision`/`display` are schema-side |
|
||||
| `bool` | Checkbox | `favourite: true` | **absent = false**; no tri-state |
|
||||
| `date` | Datum (Zeit) | `watched: 2026-08-19` / `…T14:30:00+02:00` | one type — see below |
|
||||
| `select` | Select · MultiSelect · Tags | `status: offen` · `genres: [Drama, SciFi]` | `multiple`, `open`, `options` |
|
||||
|
||||
**`date` carries its own precision.** Whether a value has a time is encoded *in the value*
|
||||
(`2026-08-19` vs `2026-08-19T14:30:00+02:00`), exactly as iCalendar distinguishes `DATE` from
|
||||
`DATE-TIME`. Consequence: **there is no `all_day` boolean** — a UI toggle switches the value's
|
||||
precision. A second flag would be a second truth that can contradict the value.
|
||||
|
||||
**`select` stores the value, not an id.** A user-defined select stores the label **verbatim**
|
||||
(`Warten auf Antwort`), because rule 4 (readable without the schema) outweighs cheap renames —
|
||||
renaming an option is an explicit "rename in N records" migration. `open: true` declares a **tag
|
||||
field**: no schema-side option list, the suggestion pool is derived from what the vault already
|
||||
uses. The universal `tags` field is exactly `{ type: select, open: true, multiple: true }`.
|
||||
|
||||
**No `duration` in the palette.** "45 Minuten Trainingszeit" is `number` + `unit: min` — indexable,
|
||||
sortable, comparable. A real `duration` type only earns its place where iCalendar demands
|
||||
ISO-8601 (`TRIGGER`), which is base-schema territory.
|
||||
|
||||
### B · References & formats
|
||||
|
||||
| Type | UI name | Frontmatter | Notes |
|
||||
|---|---|---|---|
|
||||
| `link` | Verknüpfung (ObjectRef) | `project: "[[a1b2c3]]"` | `to:` constrains the target type |
|
||||
| `url` | URL | `homepage: https://…` | |
|
||||
| `email` | E-Mail | `contact_mail: anna@example.com` | **plain value** — no label |
|
||||
| `tel` | Telefon | `hotline: "+41 44 000 00 00"` | **plain value** — no label |
|
||||
| `file` | Datei | `menu: menu.pdf` · `"[[<id>/plan.pdf]]"` | `accept` filters; no separate "image" type |
|
||||
|
||||
**`link` is the ObjectRef, and it stores the wikilink string.** Same syntax as the body
|
||||
(`"[[<id>]]"`, always by stable id, never by path or title), so there is *one* link notation in the
|
||||
file, *one* pattern for the link-graph scanner, and a human sees at a glance that the value is a
|
||||
reference. `to: <type>` makes it a **labeled edge** in the graph and lets the picker filter targets.
|
||||
|
||||
**`url` / `email` / `tel` earn their own types** (rather than `text` + a format): distinct icon,
|
||||
distinct keyboard, a distinct *action* affordance (open / mailto / tel). Their values are stored
|
||||
**verbatim as typed**. As palette fields they are **single values** (or `multiple` for a plain list)
|
||||
— the labeled `{label, value}` rows exist only on `contact` (see below), and this is the concrete
|
||||
answer to "Email ohne Label".
|
||||
|
||||
**`file` points at an asset in the bundle** — a bare relative name for an own asset, the
|
||||
`[[<id>/name]]` form for a foreign one, mirroring the embed rules in
|
||||
[`notes-and-records.md`](notes-and-records.md). A cover image is `{ type: file, accept: "image/*" }`.
|
||||
|
||||
### C · Geo
|
||||
|
||||
| Type | UI name | Frontmatter | Detail |
|
||||
|---|---|---|---|
|
||||
| `geo` | Koordinate | `geo: {lat: 46.948, lon: 7.4474, alt?: 540}` | [`geo.md`](geo.md) §1; R-Tree point |
|
||||
| `address` | Adresse | the vCard `ADR` parts **+ a nested `geo`** | single-valued, **unlabeled** in the palette |
|
||||
| `track` | Track | `{file, bbox, distance_m, ascent_m, …}` | [`geo.md`](geo.md) §3; the file is authoritative, the rest computed from it |
|
||||
|
||||
Note the distinction: the built-in **record types** `location` and `track` are *bundles*; `geo` and
|
||||
`track` here are the **field types** they are built from. A record may carry several named
|
||||
coordinate fields (`geo`, `summit`, `parking`) — that is exactly why coordinates are named
|
||||
frontmatter fields and not one hardcoded slot.
|
||||
|
||||
**`track` is the same pattern applied to a file.** The GPX/GeoJSON in the bundle is authoritative;
|
||||
`bbox`, `distance_m`, `ascent_m`, `descent_m`, `points` sit **next to `file` in the same object**,
|
||||
computed from it on import/change — so the pair reads as one unit, and a person or foreign tool sees
|
||||
"24,3 km" without parsing 4213 trackpoints. Optional start/end address labels are the
|
||||
reverse-geocoded case of rule 5 and belong there too. The summary is never hand-edited: the way to
|
||||
change a track is to replace the GPX.
|
||||
|
||||
**`address` is in the palette in its simple form** — one address, no label. It is the only palette
|
||||
type with a nested value, and it is worth the exception because it is what makes a *place* enterable
|
||||
by name (and geocodable) on a user type like `restaurant`:
|
||||
|
||||
```yaml
|
||||
address:
|
||||
street: Bahnhofstrasse 1
|
||||
postcode: "8001" # quoted — leading zeros
|
||||
locality: Zürich
|
||||
country: CH
|
||||
geo: # resolved from the parts above (rule 5)
|
||||
lat: 47.3702
|
||||
lon: 8.5401
|
||||
```
|
||||
|
||||
**The coordinate belongs to the address, so it is nested in it.** The parts are typed, `geo` is
|
||||
resolved from them — the nesting says which is which, and the two render as **one unit** (address
|
||||
text beside its mini-map). If the user corrects the pin, the entry gains `manual: true` and
|
||||
re-resolving never overwrites it silently.
|
||||
|
||||
The **derive action needs no modifier**: an `address` field always offers "Koordinaten ableiten" and
|
||||
a coordinate always offers reverse-geocoding, because that follows from the type. Resolution happens
|
||||
only on that explicit action, never while rendering ([`geo.md`](geo.md) §2) — which is also what
|
||||
keeps a rate-limited service usable.
|
||||
|
||||
A **standalone `geo` field** is the other case: a `location` record's own coordinate, or extra named
|
||||
points (`summit`, `parking`). Nothing to derive from, so it is typed or map-picked.
|
||||
|
||||
`geo`, `address` and `track` are the three palette types that are **not** click-to-edit: their
|
||||
editors need a map, sub-fields or a file dialog. Everything else in the palette is.
|
||||
|
||||
## Base-schema shapes — built-in types only
|
||||
|
||||
Not offerable in the palette. They exist because a standard demands them; a user-defined copy would
|
||||
project to `X-TERRATA-*` and be read by nothing.
|
||||
|
||||
| Shape | Frontmatter | Projection | Used by |
|
||||
|---|---|---|---|
|
||||
| **`labeled` multi-values** | `tel: [{label: Mobil, value: "+41 …"}]` | `TEL;TYPE=…` / `EMAIL` / `ADR` / `X-ABLabel` | contact |
|
||||
| **`address`, labeled + multiple** | as above, `value` is the ADR object **incl. its nested `geo`** — one coordinate per address | vCard `ADR` + `GEO` | contact |
|
||||
| `duration` | `trigger: -PT15M` | iCalendar `DURATION` / `TRIGGER` | event, task |
|
||||
| `recurrence` | `rrule: "FREQ=WEEKLY;BYDAY=TU"` + `exdate: [...]` | `RRULE` / `EXDATE` / `RECURRENCE-ID` | event, task |
|
||||
| `reminder` | `alarms: [{trigger: -PT15M, action: display}]` | `VALARM` (event *and* task) | event, task |
|
||||
| `attendee` | `attendees: [{name, email, ref?, status?}]` | `ATTENDEE;CN=…;PARTSTAT=…` | event |
|
||||
| `color` | `accent: "#E08A2B"` | — (local) | folder |
|
||||
| **codecs** | `priority: 5` shown as 3 buckets | `PRIORITY` 0–9 | task |
|
||||
| **timezone** | `tz: Europe/Zurich` | `TZID` on start/end | event |
|
||||
|
||||
- **`labeled` is the whole reason the contact editor has grips**: order = priority (topmost is
|
||||
primary), labels stored **verbatim**, mapped to `TYPE=work` / `X-ABLabel` only in the vCard
|
||||
projection ([`schema-and-types.md`](schema-and-types.md)).
|
||||
- **`recurrence` stores the RRULE string verbatim.** A structured YAML re-encoding of an already
|
||||
compact standard is a lossy translation, and the UI is a preset select anyway.
|
||||
- **`attendee` may reference a contact** (`ref`) but must not require one — an invitation cannot
|
||||
depend on the person existing as a contact bundle first.
|
||||
- **A codec is not a type.** Task `PRIORITY` stores the integer 0–9 and *displays* three buckets;
|
||||
a codec never changes the stored form the standard requires.
|
||||
- **`color` is internal for now** (the per-folder accent, and the colour of a `select` option, which
|
||||
is picked in the type editor rather than typed into a record). Promoting it to the palette later is
|
||||
cheap and breaks nothing.
|
||||
|
||||
## Schema conformance: nothing is an error, everything is visible
|
||||
|
||||
"The schema informs; it does not imprison" ([`schema-and-types.md`](schema-and-types.md)) needs a
|
||||
concrete answer for a record whose frontmatter does not match its type. That answer is: it **always
|
||||
opens, always renders, is never rejected and never silently repaired** — but the deviation is
|
||||
**visible**, and a **repair is offered**.
|
||||
|
||||
### Five kinds of deviation
|
||||
|
||||
| Kind | Example | Weight | Why that weight |
|
||||
|---|---|---|---|
|
||||
| **Undeclared** — a field too many | `mood: gut` on a type without a `mood` field | **Hinweis** | legitimate more often than not: an import, another tool, or a field typed into the file *before* declaring it |
|
||||
| **Missing** — declared, absent | `due` is declared, the file has no `due` | **nothing** (unless `required`) | absent = unset is the *normal* case (storage rule 2); an empty optional field is hidden, not flagged |
|
||||
| **Type mismatch** | `date: "Das ist definitiv kein Datum"` | **Konflikt** | the only one that really costs: the value cannot be typed, so no sort, no index, no typed editor |
|
||||
| **Constraint violation** | `rating: 15` with `max: 10`; a `select` value outside `options` | **Hinweis** | the value is perfectly usable — it just is not allowed |
|
||||
| **Missing type definition** | `type: movie`, but no `movie` schema bundle here (yet) | **Hinweis**, record-level | schemas travel as bundles, so this is the normal state *during* a sync, not a broken record |
|
||||
|
||||
Two shapes are **not** deviations at all, because we accept them by design (see "Read tolerance"): a
|
||||
scalar where `multiple` expects a list, and a bare scalar inside a `labeled` list. Cardinality is a
|
||||
shape we tolerate, not a rule we enforce.
|
||||
|
||||
**Only Konflikte are counted and badged.** This matters more than it looks: a vault with 5000 notes
|
||||
that shows 12000 warnings has effectively no warnings. Hinweise are visible where the field is, and
|
||||
nowhere else.
|
||||
|
||||
### Hard rules
|
||||
|
||||
1. **Never reject a record.** It opens and renders, always.
|
||||
2. **Never repair implicitly.** Loading a file must never write to it — same rule as the opaque block
|
||||
in the body ([`notes-and-records.md`](notes-and-records.md)): an accessory does not rewrite what
|
||||
it did not author. Every repair is a user action.
|
||||
3. **Never lose the original.** The raw text stays visible next to any proposal ("wird zu …"), and
|
||||
applying a repair is **one ordinary edit** — so whatever undo/version history exists applies to it.
|
||||
4. **Deviations are computed, never stored in the file.** They are recomputable from file + schema, so
|
||||
by storage rule 5 they live in the **index** only. A conflict list in the frontmatter would be
|
||||
absurd.
|
||||
5. **Never auto-create a schema.** For a missing type definition, do *not* infer a `movie` type from
|
||||
observed data: the real definition is on its way via sync and would then collide with our
|
||||
invention. Render the record, wait.
|
||||
|
||||
### Three levels of visibility
|
||||
|
||||
- **On the field** — the property row / pill carries a marker: amber for a Konflikt, quiet grey for a
|
||||
Hinweis, with the original value shown verbatim in mono. Clicking it opens the repair options.
|
||||
- **On the record** — a **badge in the record header** (next to ⓘ) with the *Konflikt* count, opening
|
||||
an **"Abweichungen"** section at the top of the Info panel. The header is necessary because an
|
||||
affected field may be hidden (empty/optional) or far down the panel.
|
||||
- **On the vault** — the index stores a per-field `status` (`ok` · `undeclared` · `mismatch` ·
|
||||
`violation`) plus a record-level `type_unknown` flag, so deviations are **queryable** like anything
|
||||
else: `where: { status: mismatch }`, a built-in saved query "Abweichungen", and a per-field count in
|
||||
the type editor. This is the piece that makes the whole thing maintainable — **tightening a schema
|
||||
breaks N records at once**, and you need to see the N, not stumble over them one by one.
|
||||
|
||||
### The offered repairs
|
||||
|
||||
The framing that makes this coherent: **a repair may fix the record *or* the schema — both are just
|
||||
data.** If 40 records carry text in a `number` field, it is the type definition that is wrong.
|
||||
|
||||
| Kind | Offered, in this order |
|
||||
|---|---|
|
||||
| **Type mismatch** | ① **Interpretieren** — only when a parse is plausible (`"14.9.2026"` → `2026-09-14`, `"1'322 m"` → `1322`, `"ja"` → `true`), always shown as *old → new* and confirmed. ② **Als Text behalten** — changes the *field type* to `text` in the schema. ③ **Wert löschen**. ④ Nothing: it stays a `raw` value and keeps working. |
|
||||
| **Undeclared** | ① **Als Feld übernehmen** — adds the field to the `schema` bundle, with the type **inferred from the values across all records of that type** (this makes "type it into the file, promote it later" a first-class authoring path). ② **Umbenennen auf …** — when the key is close to a declared one (`duedate` → `due`), because a typo is the most common cause. ③ **Belassen** — stays a marked `raw` row, no nagging. |
|
||||
| **Missing** | Only surfaced when `required`: ① **Feld ausfüllen** (opens the editor, pre-filled with `default`) ② **Nicht mehr erforderlich** (schema edit). Otherwise: nothing to do — it is an empty optional field. |
|
||||
| **Constraint violation** | ① **Wert anpassen** (inline edit) ② **Regel anpassen** (`max`, `options` — the schema again) ③ **Belassen**. |
|
||||
| **Missing type definition** | Nothing repairable (rule 5). The record renders with every field as `raw`, under one record-level notice. |
|
||||
|
||||
**Bulk repair** follows the same actions but from the **schema side** ("in N Records anwenden") with a
|
||||
preview list of the affected records — because that is where the cause usually sits. Phasing: the MVP
|
||||
ships **display + per-record repair**; bulk-with-preview is Phase 2 (an import of 200 German dates is
|
||||
exactly the case that earns it).
|
||||
|
||||
### `raw` is the rendering, not a diagnosis
|
||||
|
||||
An untyped value — undeclared, mismatched, or from an unknown type — renders as a **`raw` row**:
|
||||
|
||||
- a scalar → shown and editable as its inferred kind (string / number / bool / ISO date),
|
||||
- a list or object → shown **verbatim and read-only**, marked "nicht modelliert".
|
||||
|
||||
This is the frontmatter counterpart of the **opaque block** in the body editor, and it is what makes
|
||||
"a hand-edited file is never rejected" true rather than aspirational.
|
||||
|
||||
## Surfaces: how each type is rendered and edited
|
||||
|
||||
The surfaces are defined in [`ui-ux.md`](ui-ux.md): the **property pill** (inline in the body / in a
|
||||
core block), the **panel row** (Info panel), and the **field editor** (field-edit state, behind the
|
||||
header pencil).
|
||||
|
||||
**The click-to-edit rule** (this closes what `ui-ux.md` left implicit): a field is editable
|
||||
**without a mode** exactly when its value is a scalar *or* a flat list of scalars. Everything with
|
||||
structure beyond that — labels, nested objects, files — needs the field-edit state, because its
|
||||
editor needs grips, sub-fields or a file dialog. Applied to the two catalogues: **the whole palette
|
||||
except `geo`/`address`/`track` is click-to-edit; the base-schema shapes essentially are not.**
|
||||
|
||||
| Type | Pill | Click-to-edit | What hangs below the pill | Field editor |
|
||||
|---|---|---|---|---|
|
||||
| `text` | ✅ | ✅ | *nothing* (the pill is the input) | text input (`multiline` → textarea) |
|
||||
| `number` | ✅ | ✅ | nothing; unit as a suffix | number input + unit; rating stars / progress bar |
|
||||
| `bool` | ✅ | ✅ (toggles on click) | — | checkbox / toggle |
|
||||
| `date` | ✅ | ✅ | open calendar (Mo first, today outlined) + optional time | date + time inputs, all-day toggle |
|
||||
| `select` | ✅ | ✅ | option list (filter-as-you-type, "aktuell" marker) | select / chip row; `open` → tags editor |
|
||||
| `link` | ✅ | ✅ | record autocomplete (the same popover as `[[`) | record picker; `multiple` → chip row |
|
||||
| `url` | ✅ | ✅ | nothing (+ "öffnen" action) | text input with open action |
|
||||
| `email` | ✅ | ✅ | nothing (+ mailto action) | text input |
|
||||
| `tel` | ✅ | ✅ | nothing (+ tel action) | text input |
|
||||
| `file` | ✅ *(chip)* | pencil | — | file chip / dropzone (`accept`) |
|
||||
| `geo` | ✅ *(compact)* | pencil | — | lat/lon/alt inputs + map pin + derive button |
|
||||
| `address` | — *(block)* | pencil | — | ADR sub-fields + mini-map + geocode button |
|
||||
| `track` | — *(block)* | pencil | — | GPX dropzone / file chip + computed read-only summary |
|
||||
| *`labeled` (base)* | ✅ *(per entry)* | pencil | — | `[grip] [label] [value] [remove]` rows |
|
||||
| *`duration` (base)* | ✅ | ✅ | preset list (5 / 15 / 30 min · 1 h · 1 d) | number + unit select |
|
||||
| *`recurrence` (base)* | ✅ *(summary)* | pencil | — | preset select (pattern editor later) |
|
||||
| *`reminder` (base)* | — *(rows)* | pencil | — | repeatable relative-time rows |
|
||||
| *`attendee` (base)* | — *(chips)* | pencil | — | tags editor with contact autocomplete |
|
||||
| `raw` | — | scalar only | nothing | read-only verbatim + "nicht modelliert" |
|
||||
|
||||
Notes that apply across the table:
|
||||
|
||||
- **Pill vs. block.** A pill has to fit in a line of text. `address`, `track`, `reminder` and
|
||||
`attendee` therefore render as **blocks** in the core block and as **multi-line panel rows**; they
|
||||
are **not inline-referenceable** into the body in the MVP. `geo` and `recurrence` get a *compact*
|
||||
pill (`46.948, 7.447` / `wöchentlich, Di`) whose click target is the block, not an editor.
|
||||
- **Panel row = the same value, one column narrower.** Every type renders in a panel row with its
|
||||
**type icon** + key + value; the value uses the pill's rendering, wrapped instead of truncated.
|
||||
Multi-valued fields stack their entries; labeled ones show the label as the row's sub-key.
|
||||
- **Empty is hidden everywhere** except in the field-edit state, where "+ Feld" reveals a built-in
|
||||
type's remaining optional base fields (rule 2 makes this consistent with storage). For user fields
|
||||
the equivalent is "+ Eigenschaft" in the Info panel — which offers exactly the palette above.
|
||||
- **Cached values render marked ("abgeleitet"), and only some are editable.** The track statistics
|
||||
are **read-only** in *both* states — the way to change them is to replace the GPX. A geocoded
|
||||
coordinate **is** editable (drag the pin, type it), because a human correcting a wrong geocode is
|
||||
a legitimate and common case; the correction is kept as `manual: true`. System fields (`created`,
|
||||
`modified`) are read-only under their own "System" marker.
|
||||
|
||||
## Index & queries
|
||||
|
||||
What the index must do per type, so that [`queries.md`](queries.md)'s `where` / `sort` /
|
||||
`properties` work. One row per storage kind, not per type.
|
||||
|
||||
| Types | Index column | Sortable | Grouping (Kanban) | Full-text |
|
||||
|---|---|---|---|---|
|
||||
| `text`, `url`, `email`, `tel`, `color` | text | ✅ (collated) | only `select`-like values | ✅ (`text`) |
|
||||
| `number`, `duration` | numeric (duration → seconds, *derived for the index only*) | ✅ | buckets | — |
|
||||
| `bool` | integer 0/1 | ✅ | ✅ | — |
|
||||
| `date` | ISO text (sorts correctly) + a normalized instant | ✅ | by day/week/month | — |
|
||||
| `select` | text; one row per value when `multiple` | ✅ | ✅ **(the Kanban axis)** | ✅ |
|
||||
| `link` | edge row `(from, field, to)` — the labeled edge | by target title | ✅ | — |
|
||||
| `geo` | R-Tree point | — | — | — |
|
||||
| `address` | text (the ADR parts) + R-Tree point via its nested `geo` (absent until resolved) | — | — | ✅ |
|
||||
| `track` | R-Tree bbox + the numeric stats from the summary | ✅ (on the stats) | — | — |
|
||||
| `file`, `recurrence`, `reminder`, `attendee` | text (existence / summary) | — | — | — |
|
||||
| `raw` | stored, **not** indexed typed | — | — | ✅ if scalar |
|
||||
|
||||
Every indexed field row additionally carries its **conformance status** (`ok` · `undeclared` ·
|
||||
`mismatch` · `violation`), and every record a `type_unknown` flag — so deviations are queryable
|
||||
(see "Schema conformance" above). They are index-only: recomputed on every scan, never written to a
|
||||
file.
|
||||
|
||||
Two consequences worth stating: **`multiple` means a value table, not a column** (one row per
|
||||
value, `where` becomes an existence check), and **`duration` is indexed as seconds** while the file
|
||||
keeps ISO-8601 — the index is a cache, so a derived normalization there costs nothing.
|
||||
|
||||
## Deliberately *not* field types
|
||||
|
||||
- **Formula / rollup / computed fields** (Notion-style). Computed values live in the **index**
|
||||
(rule 5), never as an expression the store has to evaluate on the file. A formula has no canonical
|
||||
form on disk and would break "the file is the truth".
|
||||
- **Rich text** as a field. The one rich-text surface is the **Markdown body**, which already
|
||||
projects to `DESCRIPTION`/`NOTE`. `multiline` text is plain text.
|
||||
- **Separate `multiselect` / `multilink` / `multiemail` types** — that is the `multiple` modifier.
|
||||
- **A `time`-only type.** Sub-day precision belongs to a `date` value; a bare time without a day has
|
||||
no iCalendar counterpart.
|
||||
- **A `currency` / `percent` / `duration` type in the palette.** `number` + `unit`
|
||||
(+ `display: progress`) covers all three.
|
||||
- **User-declarable `recurrence` / `reminder` / `attendee` / labeled multi-values** — see above:
|
||||
without the standard behind them they are decoration.
|
||||
|
||||
## Open
|
||||
|
||||
- **The concrete base field list per built-in type** (which type each base field uses, and its exact
|
||||
vCard/iCalendar property) — still the open "base schema per type" work from
|
||||
[`schema-and-types.md`](schema-and-types.md). This doc fixes the *vocabulary* that list is written
|
||||
in.
|
||||
- **`address` in the palette** — proposed as single-valued and unlabeled. If a user type ever needs
|
||||
several labeled addresses, that is the first candidate for promoting `labeled` to a user-visible
|
||||
modifier.
|
||||
- **Interpretation heuristics** — which parses "Interpretieren" offers, and how ambiguity is handled:
|
||||
`14.9.2026` is unambiguous only with a locale, and `9/14/2026` vs `14/9/2026` is not. Rule already
|
||||
fixed: the proposal is always shown as *old → new* and confirmed; **an ambiguous value offers both
|
||||
readings instead of picking one**. The concrete list of heuristics (dates, thousands separators,
|
||||
`ja`/`nein`, `1`/`0`) is open.
|
||||
- **Geocoding budget** — the free service's rate limit is the reason the cache is in the file
|
||||
(rule 5). Still to design: request throttling, what happens when a *collection* of addresses is
|
||||
opened on a map (never geocode implicitly — but then unresolved records are simply missing from
|
||||
the map), whether a bulk "alle auflösen" action exists, and the provider/attribution choice.
|
||||
- **`select` option renaming** — the migration gesture ("in N Records umbenennen") is asserted here
|
||||
but not designed.
|
||||
- **`event.tz` vs. per-value zones** — the chosen shape covers `TZID` for start/end; a record needing
|
||||
two different zones (a flight!) would need more, and is deferred.
|
||||
- **Frontmatter node model** (comments + key order preservation) — a stated requirement above, with
|
||||
no library choice yet; it belongs to the same module as the Markdown round-trip.
|
||||
+38
-15
@@ -28,26 +28,46 @@ coordinates:
|
||||
- Projects cleanly to vCard `GEO` / iCal `GEO` / a GeoJSON Point.
|
||||
- Indexed in the R-Tree as a degenerate bbox (`minX=maxX=lon`, `minY=maxY=lat`).
|
||||
|
||||
## 2. Addresses — structured (vCard ADR) + derived coordinate
|
||||
## 2. Addresses — structured (vCard ADR) with a nested coordinate cache
|
||||
|
||||
- Store **structured, matching vCard `ADR`** (po-box, street, locality, region,
|
||||
postcode, country) plus an optional formatted `label` string for the worldwide
|
||||
variation no schema captures perfectly. This is the nested-structure case from the
|
||||
field index: stored faithfully, indexed shallowly.
|
||||
- **Geocoding → persist as a cache, not truth.** The address is the source; the resolved
|
||||
`coordinates: {lat, lon}` is stored *additionally* so map views and spatial queries work
|
||||
offline without re-geocoding. Re-geocode when the address changes.
|
||||
- **Geocoding → cached in the file, nested inside the address.** The typed parts are the source,
|
||||
the resolved coordinate sits **as `geo` inside the same `address` object**:
|
||||
|
||||
```yaml
|
||||
address:
|
||||
street: Bahnhofstrasse 1
|
||||
postcode: "8001"
|
||||
locality: Zürich
|
||||
country: CH
|
||||
geo: { lat: 47.3702, lon: 8.5401 } # resolved from the parts above
|
||||
```
|
||||
|
||||
Two things follow from the nesting. It makes the pair **one rendering unit** (address text
|
||||
beside its mini-map), and it answers "typed or computed?" **structurally**: everything under
|
||||
`address:` except `geo` was typed. No `derived` flag is needed, and none is used.
|
||||
- **Why the file and not just the index.** The index is **per-device and not synced**, so an
|
||||
index-only geocode means *every* device resolves *every* address again. Against a
|
||||
**rate-limited free geocoding service** that is the difference between one request per address
|
||||
per **vault** and one per address per **device** — so the cache belongs where it can be shared:
|
||||
in the record. (This is also why a stale coordinate is tolerable but a missing one is not.)
|
||||
- **Two honest caveats:** geocoding is best-effort (not every address resolves) and is a
|
||||
privacy/offline concern (it sends the address to a service). For local-first: prefer an
|
||||
offline geocoder or make it explicit **opt-in**; otherwise the derived `coordinates` stays empty.
|
||||
- **Derivation is an explicit user action, bidirectional, and cached.** In the **field-edit
|
||||
offline geocoder or make it explicit **opt-in**; otherwise the nested `coordinates` simply stays absent.
|
||||
- **Resolution is an explicit user action and bidirectional.** In the **field-edit
|
||||
state** (the header pencil, see [`ui-ux.md`](ui-ux.md)) you can enter an **address → derive
|
||||
coordinates** (geocode) *or* set **coordinates / the map pin directly**; **coordinates →
|
||||
address** (reverse-geocode) is the same pattern. The derived value is **stored (cached) in the
|
||||
record and manually overridable**, and resolution happens only **on that explicit action,
|
||||
never while rendering** — so display stays offline-safe and privacy-preserving. (Phrasing it as
|
||||
"on an explicit action" rather than "in edit mode" matters now that the body is always
|
||||
editable — there is no mode that could imply permission to hit the network.)
|
||||
address** (reverse-geocode) is the same pattern. It needs no per-field configuration — the
|
||||
affordance follows from the field type. Resolution happens only **on that explicit action, never
|
||||
while rendering** — so display stays offline-safe, privacy-preserving and inside the rate limit.
|
||||
(Phrasing it as "on an explicit action" rather than "in edit mode" matters now that the body is
|
||||
always editable — there is no mode that could imply permission to hit the network.)
|
||||
- **A hand-corrected pin is never silently overwritten.** The entry then carries
|
||||
`geo: { …, manual: true }`; a changed address only *offers* re-resolving. `manual` is written
|
||||
only when it applies — its absence means "as resolved".
|
||||
|
||||
## 3. Tracks & bulk geometry — an open-format file, NOT frontmatter
|
||||
|
||||
@@ -87,9 +107,12 @@ track:
|
||||
not hand-typed. Optional **start/end address labels** (reverse-geocoded, cached, overridable)
|
||||
may accompany the summary.
|
||||
|
||||
- The summary is a **derived cache** regenerated from the GPX on import/change — like the
|
||||
index and the geocoded coordinate, not a second *authoritative* truth (the two are never
|
||||
independently editable). Authoritative = the geometry file.
|
||||
- The summary is a **derived cache** regenerated from the GPX on import/change — not a second
|
||||
*authoritative* truth (the two are never independently editable). Authoritative = the geometry
|
||||
file. Note that this is the **same pattern as the address**: the cached values sit in the same
|
||||
object as their source (`file`), so the object itself says what is computed
|
||||
([`field-types.md`](field-types.md), storage rule 5). The payoff here is readability — a person or
|
||||
a foreign tool sees "24,3 km" without parsing 4213 trackpoints.
|
||||
- The **R-Tree indexes only the `bbox`** from the summary; the full geometry is loaded
|
||||
**only when rendering** the map. Map/nearby queries stay fast regardless of track size.
|
||||
|
||||
@@ -98,7 +121,7 @@ track:
|
||||
| Geo complexity | Stored where | R-Tree entry |
|
||||
|---|---|---|
|
||||
| Point (coordinate) | frontmatter `coordinates: {lat, lon}` | point = degenerate bbox |
|
||||
| Address | frontmatter (vCard ADR) + derived `coordinates` | via the derived `coordinates` |
|
||||
| Address | frontmatter (vCard ADR) with a **nested** `coordinates` | via that nested `coordinates` |
|
||||
| Track / area | **open-format file** in the bundle + frontmatter summary | bbox from the summary |
|
||||
|
||||
Rule of thumb: **point-level geo → fields; multi-point geometry → open file + summary.**
|
||||
|
||||
@@ -85,7 +85,7 @@ schema:
|
||||
year: { type: number }
|
||||
watched: { type: date }
|
||||
rating: { type: number, min: 0, max: 10 }
|
||||
genres: { type: list, of: text }
|
||||
genres: { type: select, options: [Drama, SciFi], multiple: true }
|
||||
```
|
||||
|
||||
- **Built-in base types** (`note`, `task`, `contact`, `event`, `location`, `track`, `folder`,
|
||||
@@ -98,6 +98,13 @@ schema:
|
||||
`link`/`to`, `coordinates`, `labeled`, `address`, `asset`, `reminder`, …) give the required typing: the field index types values correctly, the GUI picks
|
||||
the right editor/column, and query `sort`/`properties` know the type. `link`/`to` is the
|
||||
labeled-edge relation (see queries).
|
||||
- **type plus modifiers** (`multiple`, `options`, `to`, `unit`, …) — so multi-valued is a
|
||||
modifier, not a second type. **What a user may declare is a deliberately simpler subset**
|
||||
(the *palette*: 13 types, no labeled multi-values, no `RRULE`/`VALARM`/`ATTENDEE`) than what
|
||||
the built-in types use, precisely because a custom field projects to `X-TERRATA-*` and never
|
||||
into a standard property. Both catalogues — storage form, projection, rendering/editing per
|
||||
surface, index mapping — are in [`field-types.md`](field-types.md); `link`/`to` is the
|
||||
labeled-edge relation (see queries).
|
||||
|
||||
## Semantics
|
||||
|
||||
@@ -115,8 +122,11 @@ schema:
|
||||
|
||||
Some base fields are **multi-valued and labeled** — a contact has several phone numbers,
|
||||
e-mails, addresses, each with a label. Model these as an **ordered list of `{label, value}`**
|
||||
(not a scalar, and not separate `phone`/`mobile` fields). It is a distinct **field type**
|
||||
("repeatable labeled") alongside the scalar / list / link types.
|
||||
(not a scalar, and not separate `phone`/`mobile` fields). This is the **`labeled` modifier**
|
||||
(see [`field-types.md`](field-types.md)) — one storage pattern reusable on any type, not a
|
||||
type of its own. It is **base-schema only / not user-visible**: labels exist because vCard has
|
||||
`TYPE` parameters, and an `X-TERRATA-*` property has none — so a user's own `email` field is a
|
||||
plain value.
|
||||
|
||||
- **Order is meaningful = priority.** The first entry is primary; the user reorders manually
|
||||
(drag). This deliberately **replaces a PREF/preferred flag**, and the order is persisted.
|
||||
|
||||
+67
-16
@@ -37,7 +37,8 @@ theme** — so tokens here are named/structured to map onto Compose (`ColorSchem
|
||||
distinct glyph.
|
||||
- **Record view**: a **full drill-in** in its own tab. Inherits the parent folder's **background
|
||||
image** and **accent colour**. Header tools: ⓘ (Info/properties), link icon (Links), **pencil
|
||||
("Felder bearbeiten")** — the first two **switch the right panel** (no separate in-panel tab
|
||||
("Felder bearbeiten")**, plus a **conflict badge** when the record deviates from its schema (only
|
||||
then; see "Deviations from the schema" below) — the first two **switch the right panel** (no separate in-panel tab
|
||||
strip; the header buttons are the single control); the pencil switches the **core blocks** into
|
||||
the field-edit state. Title and Markdown body need **no** pencil — they are always editable
|
||||
(see "Record: the editing model").
|
||||
@@ -101,12 +102,21 @@ Properties are **always frontmatter** — only the *default rendering* differs b
|
||||
restores the previous value. Clicking straight onto the *next* attribute commits the current
|
||||
one rather than dropping the edit; clicking inside the pill being edited just moves the caret.
|
||||
- **The Info panel is editable too.** Property rows are inline field editors (same typed
|
||||
editors), and "+ Eigenschaft" adds a custom property/relation. What needs the field-edit
|
||||
editors), and "+ Eigenschaft" adds a custom property/relation — its menu *is* the **field-type
|
||||
palette** from [`field-types.md`](field-types.md) (13 entries). What needs the field-edit
|
||||
state is only the **structurally** harder work: adding a type's optional *base* fields,
|
||||
repeatable labeled multi-values, GPX, geocoding.
|
||||
- **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.
|
||||
- **Deviations from the schema are shown, never enforced.** A record whose frontmatter does not match
|
||||
its type still opens and renders normally (see "Schema conformance" in
|
||||
[`field-types.md`](field-types.md)). Three surfaces carry it: a **marker on the affected field**
|
||||
(amber = Konflikt, grey = Hinweis, original value verbatim, click opens the repair options), a
|
||||
**badge in the record header** next to ⓘ with the *Konflikt* count, and an **"Abweichungen"
|
||||
section at the top of the Info panel** listing them with their offered repairs. The badge is
|
||||
needed because the affected field may be hidden (empty/optional) or far down the panel. Nothing is
|
||||
ever repaired without a user action, and a repair may target the **schema** instead of the record.
|
||||
- **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. It is the one
|
||||
@@ -210,8 +220,16 @@ state the whole record's fields are open at once.
|
||||
**"Fertig"** button exits. Title/breadcrumb unchanged.
|
||||
- **Core-block editing is inline in the block** (not in the panel). Each base field renders as a
|
||||
**field editor** keyed by its field type (text, email, phone, date, select, checkbox, address,
|
||||
coordinates …). A **"+ Feld"** affordance reveals the type's remaining **optional base
|
||||
fields** (what the normal state hides when empty is added here).
|
||||
coordinates …) — the catalogue of types, their editors and their rendering on every surface is
|
||||
[`field-types.md`](field-types.md). A **"+ Feld"** affordance reveals the type's remaining
|
||||
**optional base fields** (what the normal state hides when empty is added here).
|
||||
- **What needs this state follows from the value's shape** (the rule behind the table above): a
|
||||
field is click-to-edit **without a mode** exactly when its value is a scalar or a flat list of
|
||||
scalars; anything with more structure — labels, nested objects, files — is edited here, because
|
||||
its editor needs grips, sub-fields or a file dialog. Applied to
|
||||
[`field-types.md`](field-types.md)'s two catalogues, this means **every user-declared field is
|
||||
click-to-edit** (the palette has nothing more structured than a list, except geo/address/track),
|
||||
so this state is essentially about the **built-in** core blocks.
|
||||
- **Repeatable, labeled fields** (tel / email / adr — vCard multi-values): a group of rows, each
|
||||
**`[grip] [label] [value] [remove]`**, with **"+ hinzufügen"**.
|
||||
- **Address is the block variant**: same `[grip] [label] [value] [remove]` line, with the
|
||||
@@ -245,8 +263,8 @@ state the whole record's fields are open at once.
|
||||
**derived from an address** (explicit "derive" button) — both directions; the pin is
|
||||
map-settable. A **track is edited via GPX upload** (dropzone / file-chip); its
|
||||
start/end/length/ascent/descent are a **read-only summary derived from the file**, shown
|
||||
with map + elevation profile; start/end addresses are reverse-geocoded (cached, overridable,
|
||||
never at display time — see [`geo.md`](geo.md)).
|
||||
with map + elevation profile; start/end addresses are reverse-geocoded on an explicit action
|
||||
(cached in the record, overridable, never at display time — see [`geo.md`](geo.md)).
|
||||
|
||||
## Colour system
|
||||
|
||||
@@ -407,7 +425,14 @@ Already exercised by the main-view wireframe — the reusable set to formalise i
|
||||
**nothing** for plain text. Never an extra field, never a closed dropdown.
|
||||
- **Inline title field** — the body H1, always editable, with the optional task checkbox before it.
|
||||
- **Field editor** — bold label + a typed input; the atom of core-block editing in the
|
||||
field-edit state.
|
||||
field-edit state. One variant **per field type** — the full list, with what each renders in the
|
||||
pill / panel row / field-edit state, is [`field-types.md`](field-types.md). Beyond the editors
|
||||
already named here, that catalogue requires: a **number editor** (unit suffix, plus the
|
||||
`rating` / `progress` displays), a **duration editor** (`N vor Beginn` — shared with the reminder
|
||||
rows), a **colour picker** (swatch row + hex; the per-folder accent and select-option colours),
|
||||
a **record picker** for `link` fields (the `[[` autocomplete popover reused as a field), a
|
||||
generalised **file picker** (the GPX dropzone with an `accept` filter), and a **type-icon set**
|
||||
(one icon per field type, for panel rows and the "+ Feld" menu).
|
||||
- **Repeatable labeled field** — group of `[grip] [label] [value] [remove]` rows (+ address
|
||||
block variant) with add/remove, drag-reorder (= priority), and a label combobox with
|
||||
used-label autocomplete.
|
||||
@@ -419,11 +444,31 @@ Already exercised by the main-view wireframe — the reusable set to formalise i
|
||||
- **Tags editor** — chips with remove + type-to-add (event attendees, multi fields).
|
||||
- **Reminder editor** — repeatable relative-time rows (`N vor/nach Fälligkeit/Beginn` = VALARM).
|
||||
- **Coordinate + map-pick editor** — lat/lon/elevation inputs with a settable map pin.
|
||||
- **Derive / geocode button** — resolves address↔coordinates on demand (result cached,
|
||||
overridable).
|
||||
- **Derive / geocode button** — resolves address↔coordinates on demand; the result is cached **inside
|
||||
the address** (one unit: address text + its map, see [`field-types.md`](field-types.md) storage
|
||||
rule 5), stays correctable by dragging the pin (`manual: true`), and a changed address only
|
||||
*offers* re-resolving. Never fires while rendering — the geocoding service is rate-limited.
|
||||
- **File upload / dropzone** — GPX file-chip (replace / remove) ↔ empty dropzone; used by tracks.
|
||||
- **Derived read-only summary** — file-derived values shown non-editable (track stats), marked
|
||||
"aus … abgeleitet".
|
||||
- **Computed read-only summary** — values calculated from a file shown non-editable (track stats),
|
||||
marked "aus … abgeleitet", in *both* states: the way to change them is to replace the source
|
||||
(the GPX). A **geocoded coordinate is the editable kind of cache** — the pin can be dragged, and
|
||||
the correction is kept (`manual: true`) so re-resolving cannot silently discard it.
|
||||
- **Raw value row** — the panel's counterpart to the opaque block: a frontmatter value with no
|
||||
schema declaration (or a shape that does not match) is shown verbatim; scalars stay editable,
|
||||
lists/objects are read-only and marked "nicht modelliert". Never dropped, never reformatted.
|
||||
- **Deviation marker** — advisory marker on a field that deviates from its schema: **amber** for a
|
||||
Konflikt (value not typeable, `required` but empty), **quiet grey** for a Hinweis (undeclared field,
|
||||
value outside `min`/`max`, unknown option). Shows the original value verbatim. It informs; it never
|
||||
corrects.
|
||||
- **Repair popover** — opened from that marker: the offered actions for this deviation kind
|
||||
(*Interpretieren* with an explicit `alt → neu` preview · *Als Text behalten* · *Als Feld übernehmen*
|
||||
· *Umbenennen auf …* · *Belassen*), per [`field-types.md`](field-types.md). Actions that change the
|
||||
**type definition** rather than the record are labelled as such.
|
||||
- **Conflict badge** (record header) — count of *Konflikte* only, next to ⓘ; opens the panel's
|
||||
Abweichungen section. Absent when the count is zero — Hinweise never raise it.
|
||||
- **Abweichungen panel section** — the per-record list, above the properties: kind, field, original
|
||||
value, actions. Plus the record-level "Typdefinition fehlt" notice (every field renders as `raw`
|
||||
until the `schema` bundle arrives via sync).
|
||||
|
||||
## Open decisions (next screens will settle these)
|
||||
|
||||
@@ -477,17 +522,23 @@ the *product* phase order lives in [`roadmap.md`](roadmap.md).
|
||||
phase-1 atoms and the mini-map placeholder, in **both states** (normal / field-edit); the
|
||||
**inline value editor** + editable **property pill**/**property row** + **inline title field**
|
||||
— these are what "work without a mode" actually requires, so they are MVP, not polish; the
|
||||
simpler **field editors** — text, **select**, **date-time + all-day**, **tags** — and the
|
||||
**"+ Feld" add-field affordance**. The gallery grows a **left nav** (the HTML styleguide's
|
||||
sidenav) as sections multiply.
|
||||
simpler **field editors** — text, **number** (unit / rating / progress), **bool**, **select**,
|
||||
**date-time + all-day**, **duration**, **colour**, **url/email/tel**, **record picker** (`link`),
|
||||
**tags** — plus the **type-icon set**, the **raw value row**, the **deviation marker + repair
|
||||
popover + conflict badge + Abweichungen panel section**, and the **"+ Feld" / "+ Eigenschaft"
|
||||
add-field affordance**. That is the **palette** of [`field-types.md`](field-types.md) minus its geo
|
||||
types — i.e. everything a user can declare, and everything that has to work click-to-edit. The
|
||||
gallery grows a **left nav** (the HTML styleguide's sidenav) as sections multiply.
|
||||
|
||||
- **Phase 3 — WYSIWYG block editor & advanced field editors.** The **block editor stack**: the
|
||||
block list with the **opaque-block fallback**, the **block handle** (⠿ drag + ⊕ insert + menu),
|
||||
the **slash-command menu**, the **selection bubble**, the **link autocomplete** (`[[`) and the
|
||||
**wikilink chip**, plus the **table grid-picker**. Then the **repeatable labeled field**
|
||||
(`[grip] [label] [value] [remove]` + drag-reorder + label autocomplete, incl. the address block
|
||||
variant), the **reminder editor** (repeatable VALARM rows), and the **GPX file upload / dropzone**
|
||||
with its **derived read-only summary**. Depends on the Markdown ↔ block-tree mapping
|
||||
variant), the **reminder editor** (repeatable VALARM rows), the **attendee editor**, the
|
||||
**recurrence preset select**, and the **file picker / dropzone** (`accept`, GPX being one case)
|
||||
with its **derived read-only summary** — i.e. the **base-schema shapes** of
|
||||
[`field-types.md`](field-types.md) (none of them user-declarable) plus the palette's file type. Depends on the Markdown ↔ block-tree mapping
|
||||
(`:core:markdown`, see [`architecture.md`](architecture.md)) — the UI is the easy half.
|
||||
|
||||
- **Phase 4 — GIS & maps (waits on `:map:api`).** Replace the mini-map placeholder with the real
|
||||
|
||||
@@ -41,6 +41,16 @@ The two files are one round-trip: the **pencil** in `record-view.html` opens `re
|
||||
the same type, and **"Fertig"** returns. Both embed the identical block editor, because the body
|
||||
does not change between them.
|
||||
|
||||
- [`field-types.html`](field-types.html) — every **field type** on one page: per type its
|
||||
**frontmatter** form (left), how it is **displayed** (property pill in the body / property row in
|
||||
the Info panel, middle) and how it is **edited** (click-to-edit without a mode / the field editor
|
||||
in the field-edit state, right). Every card is badged **Palette** (what a *user* can add — this is
|
||||
also shown as the "+ Eigenschaft" menu) or **nur Basisschema** (what only the built-in types use:
|
||||
labeled multi-values, recurrence, reminder, attendee, codecs). Opens with the two catalogues and
|
||||
the "type + modifiers" idea (`multiple` user-visible, `labeled` not) and closes with the matrix:
|
||||
pill-able · click-to-edit · index mapping. The contract is
|
||||
[`../field-types.md`](../field-types.md).
|
||||
|
||||
- [`design-system.html`](design-system.html) — the **living styleguide**: every design token
|
||||
(colour / typography / spacing / radii / elevation) and the full component inventory on one
|
||||
page, like a Storybook overview. Two switchers at the top: **Theme** (light/dark) and **Accent**
|
||||
|
||||
@@ -0,0 +1,976 @@
|
||||
<meta charset="utf-8">
|
||||
<title>Terrata — Feldtypen</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);
|
||||
--shadow-2:0 8px 20px rgba(20,20,18,.16);
|
||||
--font-display:"Space Grotesk","Avenir Next","Segoe UI",system-ui,sans-serif;
|
||||
--font-body:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
||||
--font-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
||||
--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);
|
||||
--shadow-2:0 10px 26px rgba(0,0,0,.5);
|
||||
}}
|
||||
: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);--shadow-2:0 8px 20px rgba(20,20,18,.16)}
|
||||
: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);--shadow-2:0 10px 26px rgba(0,0,0,.5)}
|
||||
|
||||
*{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}
|
||||
a{color:var(--edge-out)}
|
||||
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
|
||||
.icon{width:18px;height:18px;flex:none}
|
||||
svg{stroke-linecap:round;stroke-linejoin:round}
|
||||
code{font-family:var(--font-mono);font-size:12px;background:var(--surface-2);border:1px solid var(--line);border-radius:5px;padding:1px 6px}
|
||||
|
||||
/* ---- app chrome (identical to the other wireframes) ---- */
|
||||
.app{display:flex;flex-direction:column;min-height:100vh}
|
||||
.top{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:14px;padding:0 18px;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:6px}
|
||||
.wordmark .dot{color:var(--terra)}
|
||||
.toptitle{font-size:13px;color:var(--chrome-muted)}
|
||||
.topspace{margin-left:auto}
|
||||
.topctrls{display:flex;align-items:center;gap:16px}
|
||||
.ctl{display:flex;align-items:center;gap:8px}
|
||||
.ctl .lbl{font-size:10.5px;text-transform:uppercase;letter-spacing:.09em;color:var(--chrome-muted);font-family:var(--font-display);font-weight:600}
|
||||
.swatchbtn{width:22px;height:22px;border-radius:50%;border:2px solid transparent;padding:0;background:var(--sw);transition:transform .12s}
|
||||
.swatchbtn:hover{transform:scale(1.12)}
|
||||
.swatchbtn.sel{border-color:var(--chrome-text)}
|
||||
.themebtn{display:flex;align-items:center;gap:7px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.14);
|
||||
color:var(--chrome-text);padding:6px 12px;border-radius:999px;font-size:12px}
|
||||
|
||||
/* ---- layout ---- */
|
||||
.wrap{display:grid;grid-template-columns:210px 1fr;max-width:1320px;margin:0 auto;width:100%;flex:1;min-height:0}
|
||||
.nav{position:sticky;top:52px;align-self:start;height:calc(100vh - 52px);overflow:auto;padding:22px 12px 40px 18px}
|
||||
.nav h4{font-family:var(--font-display);font-size:10.5px;text-transform:uppercase;letter-spacing:.09em;color:var(--muted);margin:18px 6px 8px}
|
||||
.nav a{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:8px;color:var(--text);text-decoration:none;font-size:13px}
|
||||
.nav a:hover{background:var(--surface-2)}
|
||||
.nav a svg{width:15px;height:15px;color:var(--muted);flex:none}
|
||||
.nav a code{background:transparent;border:0;padding:0;color:var(--muted);font-size:11px;margin-left:auto}
|
||||
.content{padding:26px 30px 80px;min-width:0}
|
||||
|
||||
.lead{max-width:78ch;color:var(--muted);margin:2px 0 22px;font-size:14px}
|
||||
.lead b{color:var(--text)}
|
||||
section{margin:0 0 44px;scroll-margin-top:66px}
|
||||
section > h2{font-family:var(--font-display);font-size:22px;font-weight:600;margin:0 0 4px}
|
||||
section > .desc{color:var(--muted);max-width:80ch;margin:0 0 18px;font-size:13.5px}
|
||||
h3.sub{font-family:var(--font-display);font-size:10.5px;text-transform:uppercase;letter-spacing:.09em;color:var(--muted);
|
||||
margin:30px 0 12px;padding-bottom:7px;border-bottom:1px solid var(--line)}
|
||||
.demo{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);padding:18px;box-shadow:var(--shadow)}
|
||||
.demo + .demo{margin-top:14px}
|
||||
.row{display:flex;flex-wrap:wrap;gap:16px;align-items:center}
|
||||
.col{display:flex;flex-direction:column;gap:12px}
|
||||
.cap{font-size:11px;color:var(--muted);margin-top:8px;font-family:var(--font-display);letter-spacing:.02em}
|
||||
|
||||
/* ---- field-type card ---- */
|
||||
.ftcard{padding:0;overflow:hidden}
|
||||
.fthead{display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--line);background:var(--surface-2)}
|
||||
.fthead .ti{width:26px;height:26px;border-radius:7px;background:var(--surface);border:1px solid var(--line);display:grid;place-items:center;color:var(--accent);flex:none}
|
||||
.fthead .tid{font-family:var(--font-mono);font-size:12.5px;font-weight:600}
|
||||
.fthead .tnm{font-family:var(--font-display);font-weight:600;font-size:14px}
|
||||
.fthead .tmods{margin-left:auto;display:flex;gap:6px;flex-wrap:wrap}
|
||||
.modchip{font-family:var(--font-mono);font-size:10.5px;color:var(--muted);border:1px dashed var(--line-strong);border-radius:999px;padding:2px 8px}
|
||||
.ftgrid{display:grid;grid-template-columns:minmax(230px,.95fr) minmax(210px,1fr) minmax(240px,1.05fr);gap:0}
|
||||
.ftcol{padding:14px 16px;border-left:1px solid var(--line);min-width:0}
|
||||
.ftcol:first-child{border-left:0}
|
||||
.ftlbl{font-family:var(--font-display);font-size:9.5px;font-weight:600;text-transform:uppercase;letter-spacing:.09em;color:var(--muted);margin-bottom:10px}
|
||||
.ftnote{border-top:1px solid var(--line);padding:10px 16px;font-size:12px;color:var(--muted);background:var(--surface-2)}
|
||||
.ftnote b{color:var(--text)}
|
||||
@media (max-width:1080px){.ftgrid{grid-template-columns:1fr}.ftcol{border-left:0;border-top:1px solid var(--line)}.ftcol:first-child{border-top:0}}
|
||||
|
||||
pre.yaml{margin:0;font-family:var(--font-mono);font-size:11.5px;line-height:1.6;background:var(--surface-2);border:1px solid var(--line);
|
||||
border-radius:var(--r-sm);padding:10px 12px;overflow-x:auto;color:var(--text);white-space:pre}
|
||||
pre.yaml .k{color:var(--accent)}
|
||||
pre.yaml .c{color:var(--muted);font-style:italic}
|
||||
.stack{display:flex;flex-direction:column;gap:10px;align-items:flex-start}
|
||||
|
||||
/* ---- components reused from the design system ---- */
|
||||
.kv{display:inline-flex;align-items:center;gap:7px;background:var(--surface-2);border:1px solid var(--line);border-radius:999px;padding:4px 12px;font-size:12.5px;max-width:100%}
|
||||
.kv .k{color:var(--muted);flex:none} .kv .v{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.ipill{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}
|
||||
.ipill .k{color:var(--muted)} .ipill .v{font-weight:500}
|
||||
.ipill.act{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 15%,transparent)}
|
||||
.ivsel{background:color-mix(in srgb,var(--accent) 26%,transparent);border-radius:2px}
|
||||
.ivpop{display:inline-flex;flex-direction:column;background:var(--surface);border:1px solid var(--line-strong);border-radius:var(--r-sm);
|
||||
box-shadow:var(--shadow);padding:7px;min-width:190px}
|
||||
.ivlist{display:flex;flex-direction:column;gap:1px}
|
||||
.ivopt{display:flex;align-items:center;gap:7px;border:0;background:transparent;color:var(--text);padding:6px 8px;border-radius:6px;font-size:13px;text-align:left;width:100%}
|
||||
.ivopt.sel{background:var(--surface-2);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 45%,transparent)}
|
||||
.ivnow{margin-left:auto;font-size:9px;color:var(--accent);text-transform:uppercase;letter-spacing:.06em;font-family:var(--font-display);font-weight:600}
|
||||
.ivcalhd{display:flex;align-items:center;justify-content:space-between;margin:0 0 3px;font-family:var(--font-display);font-weight:600;font-size:12px}
|
||||
.ivcalhd button{border:0;background:transparent;color:var(--muted);width:20px;height:20px;border-radius:5px;font-size:14px;padding:0;line-height:1}
|
||||
.ivcal{display:grid;grid-template-columns:repeat(7,24px);gap:2px}
|
||||
.ivwd{font-size:9px;color:var(--muted);text-align:center;font-family:var(--font-display);font-weight:600}
|
||||
.ivday{border:0;background:transparent;color:var(--text);height:22px;border-radius:5px;font-size:11.5px;padding:0}
|
||||
.ivday.on{background:var(--accent);color:#fff;font-weight:600}
|
||||
.ivday.today{box-shadow:inset 0 0 0 1px var(--line-strong)}
|
||||
|
||||
.prow{display:flex;align-items:flex-start;gap:10px;max-width:300px}
|
||||
.prow .pi{width:28px;height:28px;border-radius:7px;background:var(--surface);border:1px solid var(--line);display:grid;place-items:center;color:var(--muted);flex:none}
|
||||
.prow .pk{font-size:10.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-family:var(--font-display)}
|
||||
.prow .pv{font-size:13.5px;min-width:0;overflow-wrap:anywhere}
|
||||
.prow .psub{font-size:10px;color:var(--muted);font-family:var(--font-display);text-transform:uppercase;letter-spacing:.05em}
|
||||
.panelframe{background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-sm);padding:11px 12px;display:flex;flex-direction:column;gap:10px}
|
||||
|
||||
.field{display:flex;flex-direction:column;gap:6px;max-width:320px}
|
||||
.flabel{font-family:var(--font-display);font-size:10.5px;font-weight:600;text-transform:uppercase;letter-spacing:.07em;color:var(--muted)}
|
||||
.finput{background:var(--surface);border:1px solid var(--line-strong);border-radius:var(--r-sm);padding:8px 10px;font-size:13.5px;color:var(--text);outline:none;width:100%;font-family:inherit}
|
||||
.finput:focus{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent)}
|
||||
textarea.finput{resize:vertical;min-height:64px;line-height:1.5}
|
||||
select.finput{appearance:none;background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);
|
||||
background-position:calc(100% - 15px) 16px,calc(100% - 10px) 16px;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:28px}
|
||||
.unitwrap{position:relative}
|
||||
.unitwrap .unit{position:absolute;right:10px;top:50%;transform:translateY(-50%);color:var(--muted);font-size:12px;pointer-events:none}
|
||||
.frow{display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap}
|
||||
|
||||
.toggle{width:36px;height:21px;border-radius:999px;background:var(--line-strong);position:relative;flex:none;cursor:pointer;transition:background .15s}
|
||||
.toggle.on{background:var(--accent)}
|
||||
.toggle .k{position:absolute;top:2px;left:2px;width:17px;height:17px;border-radius:50%;background:#fff;transition:left .15s}
|
||||
.toggle.on .k{left:17px}
|
||||
.togline{display:inline-flex;align-items:center;gap:10px;font-size:13px}
|
||||
.fbox{width:18px;height:18px;border-radius:5px;border:2px solid var(--accent);display:grid;place-items:center;flex:none;background:var(--accent)}
|
||||
.fbox svg{width:12px;height:12px;color:#fff}
|
||||
|
||||
.btn-ghost{display:inline-flex;align-items:center;gap:8px;background:var(--surface);color:var(--text);border:1px solid var(--line-strong);
|
||||
border-radius:999px;padding:7px 13px;font-size:12.5px}
|
||||
.btn-ghost:hover{background:var(--surface-2)}
|
||||
.addrow{display:inline-flex;align-items:center;gap:7px;border:1px dashed var(--line-strong);background:transparent;color:var(--muted);
|
||||
border-radius:999px;padding:6px 13px;font-size:12.5px;align-self:flex-start}
|
||||
.addrow:hover{color:var(--accent);border-color:var(--accent)}
|
||||
|
||||
.rlist{display:flex;flex-direction:column;gap:8px}
|
||||
.rrow{display:flex;align-items:center;gap:6px}
|
||||
.grip{color:var(--muted);cursor:grab;display:grid;place-items:center;width:20px;flex:none}
|
||||
.rlabel{width:84px;flex:none}
|
||||
.rval{flex:1;min-width:0}
|
||||
.rrem{width:28px;height:28px;border-radius:7px;border:1px solid var(--line);background:var(--surface);color:var(--muted);display:grid;place-items:center;flex:none}
|
||||
.rrem:hover{color:var(--edge-in);border-color:var(--edge-in)}
|
||||
|
||||
.tags{display:flex;flex-wrap:wrap;gap:6px;align-items:center;border:1px solid var(--line-strong);border-radius:var(--r-sm);padding:7px}
|
||||
.tagchip{display:inline-flex;align-items:center;gap:6px;background:var(--surface-2);border:1px solid var(--line);border-radius:999px;padding:3px 5px 3px 10px;font-size:12.5px}
|
||||
.tagchip button{width:17px;height:17px;border-radius:50%;border:0;background:transparent;color:var(--muted);display:grid;place-items:center;padding:0}
|
||||
.tagchip button:hover{background:var(--line);color:var(--text)}
|
||||
.tags input{border:0;outline:0;background:transparent;color:var(--text);font-size:13px;flex:1;min-width:70px;font-family:inherit}
|
||||
.chiprow{display:flex;flex-wrap:wrap;gap:6px}
|
||||
|
||||
.minimap{width:100%;max-width:230px;height:96px;border-radius:var(--r-sm);border:1px solid var(--line);position:relative;overflow:hidden;
|
||||
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:13px;height:13px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 5px color-mix(in srgb,var(--accent) 24%,transparent)}
|
||||
.minimap .route{position:absolute;inset:0}
|
||||
.minimap .route path{fill:none;stroke:var(--accent);stroke-width:2.5}
|
||||
|
||||
.filechip{display:inline-flex;align-items:center;gap:9px;background:var(--surface);border:1px solid var(--line);border-radius:var(--r-sm);padding:8px 11px;font-size:13px}
|
||||
.filechip .fi{color:var(--accent)}
|
||||
.filechip .rm{margin-left:4px;color:var(--muted)}
|
||||
.dropzone{border:1.5px dashed var(--line-strong);border-radius:var(--r);padding:16px;text-align:center;color:var(--muted);background:var(--surface);font-size:12.5px}
|
||||
.derived{display:inline-flex;align-items:center;gap:9px;background:var(--surface-2);border:1px dashed var(--line-strong);border-radius:var(--r-sm);
|
||||
padding:7px 11px;color:var(--muted);font-size:12px}
|
||||
.derived b{color:var(--text);font-weight:600}
|
||||
.ro{border:1px dashed var(--line-strong);border-radius:var(--r-sm);background:var(--surface-2);padding:7px 9px;font-size:12px;color:var(--muted);font-family:var(--font-mono)}
|
||||
.rolbl{font-family:var(--font-display);text-transform:uppercase;letter-spacing:.07em;font-size:9.5px;font-weight:600;margin-bottom:4px;color:var(--muted)}
|
||||
.warn{display:inline-flex;align-items:center;gap:7px;font-size:12px;color:var(--terra)}
|
||||
|
||||
/* rating / progress */
|
||||
.stars{display:inline-flex;gap:3px;color:var(--accent)}
|
||||
.stars .s{opacity:.28}
|
||||
.stars .s.on{opacity:1}
|
||||
.prog{width:150px;height:7px;border-radius:999px;background:var(--line);overflow:hidden}
|
||||
.prog i{display:block;height:100%;width:60%;background:var(--accent)}
|
||||
.swrow{display:flex;gap:7px;align-items:center}
|
||||
.swdot{width:22px;height:22px;border-radius:50%;border:2px solid transparent}
|
||||
.swdot.sel{border-color:var(--text)}
|
||||
.colorpill{width:13px;height:13px;border-radius:4px;flex:none;box-shadow:inset 0 0 0 1px rgba(128,128,128,.3)}
|
||||
.wlink{color:var(--edge-out);text-decoration:underline;text-underline-offset:2px;font-weight:500}
|
||||
|
||||
/* deviations */
|
||||
.dev{--dev:var(--line-strong);display:flex;gap:10px;align-items:flex-start;background:var(--surface);border:1px solid var(--line);
|
||||
border-left:3px solid var(--dev);border-radius:0 var(--r-sm) var(--r-sm) 0;padding:10px 12px}
|
||||
.dev.konflikt{--dev:#D08A2E}
|
||||
.dev .di{color:var(--dev);flex:none;margin-top:1px}
|
||||
.dev .dc{min-width:0;flex:1}
|
||||
.dev .dk{font-family:var(--font-display);font-size:9.5px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--dev)}
|
||||
.dev .dt{font-size:12.5px;margin-top:2px}
|
||||
.dev .dv{font-family:var(--font-mono);font-size:11.5px;color:var(--muted);margin-top:4px;overflow-wrap:anywhere}
|
||||
.fixrow{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
||||
.fix{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--line-strong);background:var(--surface);color:var(--text);
|
||||
border-radius:999px;padding:4px 11px;font-size:11.5px}
|
||||
.fix:hover{border-color:var(--accent);color:var(--accent)}
|
||||
.fix.prim{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
|
||||
.fix.schema{border-style:dashed}
|
||||
.fix .sc{font-size:9px;text-transform:uppercase;letter-spacing:.06em;opacity:.75}
|
||||
.arrowto{font-family:var(--font-mono);font-size:11.5px}
|
||||
.arrowto b{color:var(--accent);font-weight:600}
|
||||
.cbadge{display:inline-flex;align-items:center;gap:6px;background:color-mix(in srgb,#D08A2E 18%,transparent);color:#B4761F;
|
||||
border:1px solid color-mix(in srgb,#D08A2E 45%,transparent);border-radius:999px;padding:4px 10px;font-size:12px;font-weight:600}
|
||||
:root[data-theme="dark"] .cbadge, :root:not([data-theme="light"]) .cbadge{color:#E8A94E}
|
||||
.hdrmock{display:flex;align-items:center;gap:9px;background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-sm);padding:9px 12px}
|
||||
.hdrmock .bc{font-family:var(--font-display);font-size:12.5px;color:var(--muted)}
|
||||
.hdrmock .sp{margin-left:auto;display:flex;gap:7px;align-items:center}
|
||||
.hdrmock .ib{width:28px;height:28px;border-radius:50%;border:1px solid var(--line);background:var(--surface);color:var(--muted);display:grid;place-items:center}
|
||||
|
||||
/* palette menu ("+ Eigenschaft") */
|
||||
.palmenu{width:252px;background:var(--surface);border:1px solid var(--line-strong);border-radius:var(--r);box-shadow:var(--shadow);padding:6px;flex:none}
|
||||
.palmenu .ph{font-family:var(--font-display);text-transform:uppercase;letter-spacing:.08em;font-size:9.5px;color:var(--muted);font-weight:600;padding:7px 8px 4px}
|
||||
.palmenu .pmi{display:flex;align-items:center;gap:9px;width:100%;border:0;background:transparent;padding:6px 8px;border-radius:var(--r-sm);text-align:left;font-size:13px;color:var(--text)}
|
||||
.palmenu .pmi:hover{background:var(--surface-2)}
|
||||
.palmenu .pmi svg{color:var(--muted);width:16px;height:16px}
|
||||
.palmenu .pmi code{margin-left:auto;background:transparent;border:0;padding:0;color:var(--muted);font-size:10.5px}
|
||||
.bscope{font-family:var(--font-display);font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.07em;border-radius:999px;padding:3px 8px;flex:none}
|
||||
.bscope.pal{background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--accent)}
|
||||
.bscope.base{background:var(--surface);border:1px solid var(--line-strong);color:var(--muted)}
|
||||
|
||||
/* matrix */
|
||||
.mx{width:100%;border-collapse:collapse;font-size:12.5px}
|
||||
.mx th,.mx td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line);vertical-align:top}
|
||||
.mx th{font-family:var(--font-display);font-size:9.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-weight:600;
|
||||
position:sticky;top:52px;background:var(--surface);z-index:1}
|
||||
.mx td.t{font-family:var(--font-mono);font-size:12px;white-space:nowrap}
|
||||
.mx td.c{text-align:center;white-space:nowrap}
|
||||
.mx tr:last-child td{border-bottom:0}
|
||||
.yes{color:var(--accent);font-weight:600}
|
||||
.no{color:var(--muted)}
|
||||
.grp td{background:var(--surface-2);font-family:var(--font-display);font-size:9.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-weight:600}
|
||||
.maprow{display:flex;gap:10px;align-items:flex-start;font-size:12.5px;color:var(--muted);background:var(--surface-2);border:1px solid var(--line);
|
||||
border-left:3px solid var(--accent);border-radius:0 var(--r-sm) var(--r-sm) 0;padding:10px 14px;margin-top:16px}
|
||||
.maprow b{color:var(--text)}
|
||||
</style>
|
||||
|
||||
<div class="app">
|
||||
<header class="top">
|
||||
<span class="wordmark">terrata<span class="dot">.</span></span>
|
||||
<span class="toptitle">Feldtypen</span>
|
||||
<span class="topspace"></span>
|
||||
<div class="topctrls">
|
||||
<div class="ctl">
|
||||
<span class="lbl">Accent</span>
|
||||
<button class="swatchbtn sel" style="--sw:#2F7A64" data-accent="#2F7A64" data-accent-d="#4E9E86" title="Pine (Home / Default)"></button>
|
||||
<button class="swatchbtn" style="--sw:#E08A2B" data-accent="#E08A2B" data-accent-d="#E8A04E" title="Ordner 1"></button>
|
||||
<button class="swatchbtn" style="--sw:#3E7CB1" data-accent="#3E7CB1" data-accent-d="#5C97C9" title="Ordner 2"></button>
|
||||
<button class="swatchbtn" style="--sw:#9B5DE5" data-accent="#9B5DE5" data-accent-d="#B487EC" title="Ordner 3"></button>
|
||||
</div>
|
||||
<button class="themebtn" id="themebtn"><span id="themeicon"></span><span id="themetxt">Dunkel</span></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
<nav class="nav" id="nav">
|
||||
<h4>Grundlagen</h4>
|
||||
<a href="#catalogues">Zwei Kataloge</a>
|
||||
<a href="#axes">Zwei Achsen</a>
|
||||
<a href="#conformance">Konformität</a>
|
||||
<a href="#matrix">Matrix</a>
|
||||
</nav>
|
||||
|
||||
<main class="content">
|
||||
<p class="lead">Jeder <b>Feldtyp</b> auf einer Seite — links wie er im <b>Frontmatter</b> steht, in der Mitte
|
||||
wie er <b>angezeigt</b> wird (Property-Pill im Body / Property-Row im Info-Panel), rechts wie er
|
||||
<b>bearbeitet</b> wird (klick-direkt ohne Modus bzw. Field-Editor im Feld-Edit-Zustand). Der Vertrag dazu ist
|
||||
<a href="../field-types.md">field-types.md</a>; Oberflächen und Tokens kommen aus
|
||||
<a href="../ui-ux.md">ui-ux.md</a> / <a href="design-system.html">design-system.html</a>.</p>
|
||||
|
||||
<section id="catalogues">
|
||||
<h2>Zwei Kataloge, nicht einer</h2>
|
||||
<p class="desc">Links die <b>Palette</b> — was ein <i>Benutzer</i> an einen Typ anbauen kann; das ist
|
||||
gleichzeitig das „+ Eigenschaft"-Menü. Rechts, warum sie einfacher sein <i>darf</i>: ein eigenes Feld
|
||||
projiziert nach <code>X-TERRATA-*</code> und nie in eine Standard-Property. Labels
|
||||
(<code>{label, value}</code>) existieren nur, weil vCard <code>TYPE</code>-Parameter hat — ein
|
||||
<code>X-</code>-Property hat keine, also ist ein Benutzer-<code>email</code>-Feld ein <b>blosser Wert</b>.
|
||||
Dasselbe gilt für <code>RRULE</code>/<code>VALARM</code>/<code>ATTENDEE</code>: ohne Standard dahinter wären
|
||||
sie Dekoration.</p>
|
||||
<div class="demo">
|
||||
<div class="row" style="align-items:flex-start;gap:26px">
|
||||
<div>
|
||||
<div class="ftlbl">„+ Eigenschaft" — die Palette</div>
|
||||
<div class="palmenu" id="palette"></div>
|
||||
</div>
|
||||
<div style="flex:1;min-width:300px">
|
||||
<div class="ftlbl">Palette vs. Basisschema</div>
|
||||
<table class="mx" id="cmp"></table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ftnote" style="margin:16px -18px -18px;border-bottom:0"><b>Folge:</b> kein Palette-Typ hat
|
||||
Struktur jenseits von „ein Wert oder eine Liste von Werten" — also ist <b>jedes benutzerdefinierte Feld
|
||||
direkt editierbar</b>, ohne Modus. Der Feld-Edit-Zustand (Bleistift) bleibt fast ausschliesslich für die
|
||||
<i>eingebauten</i> Kern-Blöcke.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="axes">
|
||||
<h2>Zwei Achsen: Typ + Modifikatoren</h2>
|
||||
<p class="desc">Ein Feld ist ein <b>Typ plus Modifikatoren</b> — „MultiSelect" ist kein eigener Typ, sondern
|
||||
<code>select</code> + <code>multiple</code>. Mehrwertigkeit braucht fast jeder Typ (mehrere Links, Tags,
|
||||
Dateien); ein Typ pro Kombination würde die Liste verdoppeln, ohne etwas Neues zu sagen. <code>multiple</code>
|
||||
ist benutzersichtbar — <code>labeled</code> (rechts) <b>nicht</b>: das ist die vCard-Form und bleibt im
|
||||
Basisschema.</p>
|
||||
<div class="demo">
|
||||
<div class="ftgrid">
|
||||
<div class="ftcol">
|
||||
<div class="ftlbl">einfach</div>
|
||||
<pre class="yaml"><span class="k">email:</span> anna@example.com</pre>
|
||||
<div style="margin-top:10px" class="stack">
|
||||
<span class="kv"><span class="k">E-Mail</span><span class="v">anna@example.com</span></span>
|
||||
<div class="field"><input class="finput" value="anna@example.com"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ftcol">
|
||||
<div class="ftlbl">multiple: true</div>
|
||||
<pre class="yaml"><span class="k">email:</span>
|
||||
- anna@example.com
|
||||
- anna@home.ch</pre>
|
||||
<div style="margin-top:10px" class="stack">
|
||||
<div class="chiprow"><span class="kv"><span class="v">anna@example.com</span></span><span class="kv"><span class="v">anna@home.ch</span></span></div>
|
||||
<div class="tags"><span class="tagchip">anna@example.com <button id="mx1"></button></span><span class="tagchip">anna@home.ch <button id="mx2"></button></span><input placeholder="hinzufügen …"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ftcol">
|
||||
<div class="ftlbl">labeled: true — nur Basisschema (Reihenfolge = Priorität)</div>
|
||||
<pre class="yaml"><span class="k">email:</span>
|
||||
- <span class="k">label:</span> Arbeit
|
||||
<span class="k">value:</span> anna@example.com
|
||||
- <span class="k">label:</span> Privat
|
||||
<span class="k">value:</span> anna@home.ch</pre>
|
||||
<div style="margin-top:10px" class="rlist">
|
||||
<div class="rrow"><span class="grip" id="ax1"></span><input class="finput rlabel" value="Arbeit"><input class="finput rval" value="anna@example.com"><button class="rrem" id="ax2"></button></div>
|
||||
<div class="rrow"><span class="grip" id="ax3"></span><input class="finput rlabel" value="Privat"><input class="finput rval" value="anna@home.ch"><button class="rrem" id="ax4"></button></div>
|
||||
<button class="addrow" id="ax5"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ftnote"><b>Lese-Toleranz:</b> ein Skalar dort, wo eine Liste erwartet wird, gilt als
|
||||
Ein-Element-Liste; ein blosser Skalar in einer labeled-Liste ist ein Eintrag ohne Label. Normalisiert wird
|
||||
<b>nur das Feld, das der Benutzer bearbeitet</b> — nie als Nebenwirkung.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div id="cards"></div>
|
||||
|
||||
<section id="conformance">
|
||||
<h2>Schema-Konformität — nichts ist ein Fehler</h2>
|
||||
<p class="desc">Ein Record, dessen Frontmatter nicht zur Typdefinition passt, <b>öffnet und
|
||||
rendert immer</b>. Er wird nie abgelehnt und nie stillschweigend repariert — aber die Abweichung ist
|
||||
<b>sichtbar</b>, und eine Reparatur wird <b>angeboten</b>. Wichtig: eine Reparatur darf auch die
|
||||
<b>Typdefinition</b> treffen statt den Record — wenn 40 Records Text in einem <code>number</code>-Feld
|
||||
haben, ist die Definition falsch. Solche Aktionen sind unten <span class="fix schema"
|
||||
style="padding:2px 8px">gestrichelt</span> markiert.</p>
|
||||
<p class="desc"><b>Gezählt und gebadgt werden nur Konflikte.</b> Ein Vault mit 5000 Notizen, der 12000
|
||||
Warnungen zeigt, hat effektiv keine Warnungen — Hinweise erscheinen nur dort, wo das Feld steht.</p>
|
||||
|
||||
<h3 class="sub">Die fünf Arten</h3>
|
||||
<div class="demo" id="devlist" style="display:flex;flex-direction:column;gap:10px"></div>
|
||||
|
||||
<h3 class="sub">Drei Sichtbarkeits-Ebenen</h3>
|
||||
<div class="demo">
|
||||
<div class="row" style="align-items:flex-start;gap:22px">
|
||||
<div style="flex:1;min-width:260px">
|
||||
<div class="ftlbl">1 · am Feld</div>
|
||||
<div class="stack">
|
||||
<span class="ipill" style="border-color:#D08A2E"><span class="k">Fällig</span><span class="v" style="font-family:var(--font-mono);font-size:11.5px">"Das ist definitiv kein Datum"</span><span id="dm1" style="color:#D08A2E;display:flex"></span></span>
|
||||
<span style="font-size:11.5px;color:var(--muted)">Klick auf den Marker öffnet die Reparatur-Optionen</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex:1;min-width:260px">
|
||||
<div class="ftlbl">2 · am Record (Header)</div>
|
||||
<div class="stack">
|
||||
<div class="hdrmock"><span class="bc">⌂ / Aufgabe / Steuererklärung</span>
|
||||
<span class="sp"><span class="cbadge" id="cb1">1</span><span class="ib" id="hi1"></span><span class="ib" id="hi2"></span></span></div>
|
||||
<span style="font-size:11.5px;color:var(--muted)">Nötig, weil das betroffene Feld leer/ausgeblendet oder weit unten sein kann.
|
||||
Badge fehlt bei null Konflikten</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-top:18px;align-items:flex-start;gap:22px">
|
||||
<div style="flex:1;min-width:260px">
|
||||
<div class="ftlbl">2b · Panel-Sektion „Abweichungen"</div>
|
||||
<div class="panelframe" style="max-width:320px">
|
||||
<div style="font-family:var(--font-display);font-weight:600;font-size:12.5px;display:flex;align-items:center;gap:7px"><span id="dp1" style="color:#D08A2E;display:flex"></span> Abweichungen</div>
|
||||
<div class="dev konflikt"><span class="di" id="dp2"></span><div class="dc"><div class="dk">Typ passt nicht</div><div class="dt"><b>due</b> — kein Datum</div><div class="dv">"Das ist definitiv kein Datum"</div></div></div>
|
||||
<div class="dev"><span class="di" id="dp3"></span><div class="dc"><div class="dk">Nicht deklariert</div><div class="dt"><b>mood</b></div><div class="dv">gut</div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex:1;min-width:260px">
|
||||
<div class="ftlbl">3 · im Vault (abfragbar)</div>
|
||||
<pre class="yaml">```terrata-query
|
||||
<span class="k">where:</span>
|
||||
<span class="k">status:</span> mismatch
|
||||
<span class="k">properties:</span> [link, type, field]
|
||||
```</pre>
|
||||
<span class="cap">Der Index führt pro Feld einen <code>status</code> (<code>ok · undeclared ·
|
||||
mismatch · violation</code>) und pro Record ein <code>type_unknown</code>-Flag. <b>Das</b> ist das
|
||||
Stück, das die Sache wartbar macht: eine verschärfte Typdefinition bricht N Records auf einmal —
|
||||
man will die N sehen, nicht über sie stolpern.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="maprow" style="margin-top:16px">
|
||||
<span id="hardicon"></span>
|
||||
<div><b>Harte Regeln:</b> nie ablehnen · <b>nie implizit reparieren</b> (Laden schreibt nie — dieselbe
|
||||
Regel wie beim Opaque-Block) · das Original bleibt sichtbar, jede Reparatur ist <i>eine</i> normale
|
||||
Änderung (Undo gilt) · Abweichungen leben nur im <b>Index</b>, nie in der Datei · <b>nie ein Schema
|
||||
erfinden</b>: bei fehlender Typdefinition kommt die echte per Sync — eine geratene würde mit ihr
|
||||
kollidieren.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="matrix">
|
||||
<h2>Matrix</h2>
|
||||
<p class="desc">Dieselben Typen auf einen Blick: Pill-fähig (passt in eine Textzeile), ohne Modus editierbar
|
||||
(Wert ist ein Skalar oder eine flache Liste von Skalaren) und die Index-Abbildung für
|
||||
<a href="../queries.md">queries.md</a>.</p>
|
||||
<div class="demo" style="padding:0;overflow:hidden">
|
||||
<table class="mx" id="mxtable"></table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="maprow">
|
||||
<span id="cmpicon"></span>
|
||||
<div><b>Nächster Schritt:</b> Die Editoren hier sind der Bauplan für die <code>:ui</code>-Komponenten
|
||||
(Phase 2 = Skalare/Choice/Referenzen, Phase 3 = repeatable/labeled + Datei, Phase 4 = GIS) —
|
||||
siehe <a href="../ui-ux.md">ui-ux.md</a>, „Compose build-out phases".</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ---------- inline SVG icons ---------- */
|
||||
const sv = (b) => '<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">'+b+'</svg>';
|
||||
const I = {
|
||||
text: sv('<path d="M4 6h16M4 12h12M4 18h8"/>'),
|
||||
hash: sv('<path d="M9 3 7 21M17 3l-2 18M4 8h17M3 16h17"/>'),
|
||||
check: sv('<path d="M20 6 9 17l-5-5"/>'),
|
||||
calendar: sv('<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/>'),
|
||||
clock: sv('<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>'),
|
||||
droplet: sv('<path d="M12 3s6 6.5 6 11a6 6 0 0 1-12 0c0-4.5 6-11 6-11Z"/>'),
|
||||
list: sv('<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/>'),
|
||||
link: sv('<path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/>'),
|
||||
globe: sv('<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3.5 3 14 0 18M12 3c-3 3.5-3 14 0 18"/>'),
|
||||
mail: sv('<rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/>'),
|
||||
phone: sv('<path d="M4 4h4l2 5-3 2a12 12 0 0 0 6 6l2-3 5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 2 6a2 2 0 0 1 2-2Z"/>'),
|
||||
pin: sv('<path d="M12 21s7-6.5 7-12a7 7 0 1 0-14 0c0 5.5 7 12 7 12Z"/><circle cx="12" cy="9" r="2.5"/>'),
|
||||
home: sv('<path d="m3 10 9-7 9 7"/><path d="M5 9v11h14V9"/>'),
|
||||
track: sv('<path d="M5 19c0-3 3-3 3-6s-3-3-3-6"/><path d="M19 5c0 3-3 3-3 6s3 3 3 6"/><circle cx="5" cy="5" r="1.6"/><circle cx="19" cy="19" r="1.6"/>'),
|
||||
file: sv('<path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9Z"/><path d="M14 3v6h6"/>'),
|
||||
repeat: sv('<path d="M17 2l4 4-4 4"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><path d="M7 22l-4-4 4-4"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/>'),
|
||||
bell: sv('<path d="M18 9a6 6 0 1 0-12 0c0 6-3 7-3 7h18s-3-1-3-7"/><path d="M10.5 20a2 2 0 0 0 3 0"/>'),
|
||||
users: sv('<circle cx="9" cy="8" r="3.2"/><path d="M2.5 20a6.5 6.5 0 0 1 13 0"/><path d="M16 5.3a3.2 3.2 0 0 1 0 5.4M17.5 14.2A6.5 6.5 0 0 1 21.5 20"/>'),
|
||||
braces: sv('<path d="M8 3C5.5 3 6.5 10 3.5 10 6.5 10 5.5 21 8 21"/><path d="M16 3c2.5 0 1.5 7 4.5 7-3 0-2 11-4.5 11"/>'),
|
||||
x: sv('<path d="M18 6 6 18M6 6l12 12"/>'),
|
||||
plus: sv('<path d="M12 5v14M5 12h14"/>'),
|
||||
grip: '<svg class="icon" viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="9" cy="6" r="1.6"/><circle cx="15" cy="6" r="1.6"/><circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/><circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="18" r="1.6"/></svg>',
|
||||
upload: sv('<path d="M12 15V3m0 0-4 4m4-4 4 4"/><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/>'),
|
||||
crosshair: sv('<circle cx="12" cy="12" r="8"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/>'),
|
||||
sparkles: sv('<path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l2 2M16 16l2 2M18 6l-2 2M8 16l-2 2"/>'),
|
||||
warn: sv('<path d="M12 4 2 20h20L12 4Z"/><path d="M12 10v4M12 17h.01"/>'),
|
||||
info: sv('<circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/>'),
|
||||
unknown: sv('<circle cx="12" cy="12" r="9"/><path d="M9.6 9.4a2.5 2.5 0 1 1 3.4 2.4c-.7.4-1 .9-1 1.7M12 17h.01"/>'),
|
||||
sun: sv('<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>'),
|
||||
moon: sv('<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z"/>'),
|
||||
star: '<svg class="icon" viewBox="0 0 24 24" fill="currentColor" stroke="none" style="width:14px;height:14px"><path d="m12 3 2.6 5.6 6 .7-4.4 4.1 1.2 6L12 16.6 6.6 19.4l1.2-6L3.4 9.3l6-.7Z"/></svg>',
|
||||
};
|
||||
const set=(id,html)=>{const e=document.getElementById(id);if(e)e.innerHTML=html;};
|
||||
|
||||
/* helper snippets */
|
||||
const cal = `<span class="ivpop">
|
||||
<span class="ivcalhd"><button>‹</button><span>August 2026</span><button>›</button></span>
|
||||
<span class="ivcal">
|
||||
<span class="ivwd">Mo</span><span class="ivwd">Di</span><span class="ivwd">Mi</span><span class="ivwd">Do</span><span class="ivwd">Fr</span><span class="ivwd">Sa</span><span class="ivwd">So</span>
|
||||
<button class="ivday" style="visibility:hidden"></button><button class="ivday" style="visibility:hidden"></button>
|
||||
<button class="ivday">10</button><button class="ivday">11</button><button class="ivday">12</button><button class="ivday">13</button><button class="ivday">14</button>
|
||||
<button class="ivday">15</button><button class="ivday">16</button><button class="ivday">17</button><button class="ivday today">18</button>
|
||||
<button class="ivday on">19</button><button class="ivday">20</button><button class="ivday">21</button>
|
||||
</span></span>`;
|
||||
const opts = (list, cur) => '<span class="ivpop"><span class="ivlist">' + list.map(o =>
|
||||
'<button class="ivopt' + (o===cur?' sel':'') + '">' + o + (o===cur?'<span class="ivnow">aktuell</span>':'') + '</button>').join('') + '</span></span>';
|
||||
const prow = (icon,k,v,sub) => '<div class="prow"><span class="pi">'+icon+'</span><div style="min-width:0">'+
|
||||
(sub?'<div class="pk">'+k+'</div><div class="psub">'+sub+'</div>':'<div class="pk">'+k+'</div>')+'<div class="pv">'+v+'</div></div></div>';
|
||||
const fld = (label, inner) => '<div class="field"><span class="flabel">'+label+'</span>'+inner+'</div>';
|
||||
const inp = (v) => '<input class="finput" value="'+v+'">';
|
||||
|
||||
/* ---------- the catalogue (single source for cards + matrix) ---------- */
|
||||
const GROUPS = [
|
||||
{id:'scalars', title:'A · Palette — Skalare', scope:'pal', desc:'Ein Wert, ein Skalar im Frontmatter. Alle ohne Modus editierbar — die Pille <i>ist</i> das Eingabefeld. <code>select</code> deckt Select, MultiSelect (<code>multiple</code>) und Tags (<code>open</code>) ab.'},
|
||||
{id:'refs', title:'B · Palette — Referenzen & Formate', scope:'pal', desc:'Zeigen auf etwas anderes: ein Bundle, eine Datei im Bundle, eine Adresse in der Welt. <b>Ohne Label</b> — die labeled-Zeilen gibt es nur im Basisschema.'},
|
||||
{id:'geo', title:'C · Palette — Geo', scope:'pal', desc:'Speicherregeln aus <a href="../geo.md">geo.md</a>: Punkt ins Frontmatter, Geometrie als offene Datei + Summary. Die einzigen Palette-Typen, die den Bleistift brauchen (Karte, Unterfelder, Datei-Dialog). <b>Berechnete Werte sind gecacht — in der Datei, im Objekt ihrer Quelle:</b> <code>address.geo</code> neben den getippten Teilen, <code>track.bbox/distance_m/…</code> neben <code>track.file</code>. Die <b>Position</b> sagt damit, was berechnet ist (kein Flag nötig), und Quelle + Cache sind <b>eine Render-Einheit</b>. In der Datei statt nur im Index, weil der Index pro Gerät neu gebaut und <b>nicht</b> synchronisiert wird — bei einem rate-limitierten Gratis-Geocoder wäre das eine Abfrage pro Adresse pro <i>Gerät</i> statt pro <i>Vault</i>.'},
|
||||
{id:'std', title:'D · Nur Basisschema', scope:'base', desc:'Nicht in der Palette. Existieren, weil ein Standard sie fordert — eine benutzerdefinierte Kopie würde nach <code>X-TERRATA-*</code> projizieren und von nichts gelesen.'},
|
||||
{id:'raw', title:'E · Fallback', scope:'base', desc:'Das Gegenstück zum Opaque-Block: nicht deklarierte oder nicht passende Werte gehen nie verloren.'},
|
||||
];
|
||||
|
||||
const T = [
|
||||
/* ---------------- A · scalars ---------------- */
|
||||
{g:'scalars', id:'text', name:'Text', icon:I.text, mods:['multiline','multiple'],
|
||||
yaml:`<span class="k">title:</span> Hardergrat-Tour
|
||||
<span class="k">synopsis:</span> |
|
||||
Zwei Tage, ein Grat,
|
||||
viel zu wenig Wasser.`,
|
||||
view:`<div class="stack">
|
||||
<span class="kv"><span class="k">Titel</span><span class="v">Hardergrat-Tour</span></span>
|
||||
<div class="panelframe">`+prow(I.text,'Synopsis','Zwei Tage, ein Grat, viel zu wenig Wasser.')+`</div></div>`,
|
||||
edit:`<div class="stack">
|
||||
<span class="ipill act"><span class="k">Titel</span><span class="v ivsel">Hardergrat-Tour</span></span>
|
||||
<span style="font-size:11.5px;color:var(--muted)">↑ klick-direkt: <b>kein</b> Popover — die Pille ist der Editor</span>
|
||||
`+fld('Synopsis','<textarea class="finput">Zwei Tage, ein Grat, viel zu wenig Wasser.</textarea>')+`</div>`,
|
||||
note:'<b>multiline</b> serialisiert als YAML-Block-Skalar (<code>|</code>) — bleibt lesbar und diffbar. Projektion: keine (der Body ist <code>DESCRIPTION</code>/<code>NOTE</code>).',
|
||||
pill:'✅', cte:'✅', idx:'text · FTS'},
|
||||
|
||||
{g:'scalars', id:'number', name:'Zahl', icon:I.hash, mods:['unit','min/max','precision','display'],
|
||||
yaml:`<span class="k">alt:</span> 1322 <span class="c"># unit: m — im Schema</span>
|
||||
<span class="k">rating:</span> 8 <span class="c"># min 0 max 10, display: rating</span>
|
||||
<span class="k">progress:</span> 60 <span class="c"># display: progress</span>`,
|
||||
view:`<div class="stack">
|
||||
<span class="kv"><span class="k">Höhe</span><span class="v">1322 m ü. M.</span></span>
|
||||
<span class="kv"><span class="k">Rating</span><span class="v stars">`+I.star.repeat(0)+`<span class="s on">`+I.star+`</span><span class="s on">`+I.star+`</span><span class="s on">`+I.star+`</span><span class="s on">`+I.star+`</span><span class="s">`+I.star+`</span></span></span>
|
||||
<span class="kv"><span class="k">Fortschritt</span><span class="v"><span class="prog"><i></i></span></span></span></div>`,
|
||||
edit:`<div class="stack">
|
||||
`+fld('Höhe','<span class="unitwrap"><input class="finput" value="1322"><span class="unit">m</span></span>')+`
|
||||
<span class="ipill act"><span class="k">Rating</span><span class="v ivsel">8</span></span></div>`,
|
||||
note:'<b>Die Einheit ist Schema-Metadatum</b>, der Wert steht immer in der kanonischen Einheit (Distanzen in m). Nur so bleiben Vergleich und Sortierung im Index korrekt.',
|
||||
pill:'✅', cte:'✅', idx:'numerisch'},
|
||||
|
||||
{g:'scalars', id:'bool', name:'Checkbox', icon:I.check, mods:['default'],
|
||||
yaml:`<span class="k">favourite:</span> true
|
||||
<span class="c"># fehlt = false — kein null, kein Tri-State</span>`,
|
||||
view:`<div class="stack">
|
||||
<span class="kv"><span class="k">Favorit</span><span class="v" style="display:inline-flex;align-items:center"><span class="fbox">`+I.check+`</span></span></span>
|
||||
<div class="panelframe">`+prow(I.check,'Favorit','<span class="togline"><span class="toggle on"><span class="k"></span></span> ja</span>')+`</div></div>`,
|
||||
edit:`<div class="stack">
|
||||
<span class="togline"><span class="toggle on"><span class="k"></span></span> Favorit</span>
|
||||
<span style="font-size:11.5px;color:var(--muted)">Ein Klick schaltet um — kein Popover, kein Speichern</span></div>`,
|
||||
note:'<b>Fehlender Key = false.</b> Damit gilt „leere optionale Felder werden nicht gespeichert" auch hier, und ein Tri-State kann nie entstehen.',
|
||||
pill:'✅', cte:'✅', idx:'0/1'},
|
||||
|
||||
{g:'scalars', id:'date', name:'Datum (Zeit)', icon:I.calendar, mods:['multiple','min/max'],
|
||||
yaml:`<span class="k">due:</span> 2026-08-19 <span class="c"># nur Datum</span>
|
||||
<span class="k">start:</span> 2026-08-19T14:30:00+02:00
|
||||
<span class="k">tz:</span> Europe/Zurich <span class="c"># nur event (TZID)</span>`,
|
||||
view:`<div class="stack">
|
||||
<span class="kv"><span class="k">Fällig</span><span class="v">19. Aug 2026</span></span>
|
||||
<span class="kv"><span class="k">Beginn</span><span class="v">19. Aug 2026, 14:30</span></span></div>`,
|
||||
edit:`<div class="stack">
|
||||
<span class="ipill act"><span class="k">Fällig</span><span class="v ivsel">19.08.2026</span></span>
|
||||
`+cal+`
|
||||
<div class="frow">`+fld('Datum','<input class="finput" type="date" value="2026-08-19">')+fld('Zeit','<input class="finput" type="time" value="14:30">')+`</div>
|
||||
<span class="togline"><span class="toggle"><span class="k"></span></span> Ganztägig</span></div>`,
|
||||
note:'<b>Die Präzision steckt im Wert</b> (<code>DATE</code> vs. <code>DATE-TIME</code>) — deshalb gibt es <b>kein <code>all_day</code>-Flag</b>: der Ganztägig-Schalter ändert die Präzision, nicht eine zweite Wahrheit.',
|
||||
pill:'✅', cte:'✅', idx:'ISO-Text + Instant'},
|
||||
|
||||
{g:'std', id:'duration', name:'Dauer', icon:I.clock, mods:['multiple'],
|
||||
yaml:`<span class="k">trigger:</span> -PT15M <span class="c"># ISO-8601, negativ erlaubt</span>
|
||||
<span class="k">duration_s:</span> 28800 <span class="c"># abgeleitete Statistik = number</span>`,
|
||||
view:`<div class="stack">
|
||||
<span class="kv"><span class="k">Erinnerung</span><span class="v">15 Min vor Beginn</span></span>
|
||||
<span class="kv"><span class="k">Dauer</span><span class="v">8 h 00</span></span></div>`,
|
||||
edit:`<div class="stack">
|
||||
<span class="ipill act"><span class="k">Erinnerung</span><span class="v ivsel">15 Min</span></span>
|
||||
`+opts(['5 Minuten','15 Minuten','30 Minuten','1 Stunde','1 Tag'],'15 Minuten')+`
|
||||
<div class="frow">`+fld('Vorlauf','<input class="finput" value="15">')+fld('Einheit','<select class="finput"><option>Minuten</option><option>Stunden</option><option>Tage</option></select>')+`</div></div>`,
|
||||
note:'ISO-8601 dort, wo iCalendar <code>DURATION</code>/<code>TRIGGER</code> erwartet (verlustfreier Round-Trip). <b>Nicht in der Palette:</b> „45 Minuten Trainingszeit" ist <code>number</code> + <code>unit: min</code> — indexierbar und sortierbar. Eine <i>abgeleitete</i> Statistik wie <code>duration_s</code> ist deshalb auch eine <code>number</code>.',
|
||||
pill:'✅', cte:'✅', idx:'numerisch (s)'},
|
||||
|
||||
{g:'std', id:'color', name:'Farbe', icon:I.droplet, mods:[],
|
||||
yaml:`<span class="k">accent:</span> "#E08A2B" <span class="c"># folder-Accent, select-Option</span>`,
|
||||
view:`<div class="stack">
|
||||
<span class="kv"><span class="colorpill" style="background:#E08A2B"></span><span class="k">Accent</span><span class="v">#E08A2B</span></span>
|
||||
<div class="panelframe">`+prow(I.droplet,'Accent','<span class="swrow"><span class="swdot sel" style="background:#E08A2B"></span> #E08A2B</span>')+`</div></div>`,
|
||||
edit:`<div class="stack">
|
||||
<div class="swrow">
|
||||
<span class="swdot" style="background:#2F7A64"></span><span class="swdot sel" style="background:#E08A2B"></span>
|
||||
<span class="swdot" style="background:#3E7CB1"></span><span class="swdot" style="background:#9B5DE5"></span><span class="swdot" style="background:#C85C3C"></span>
|
||||
</div>
|
||||
`+fld('Hex','<input class="finput" value="#E08A2B">')+`</div>`,
|
||||
note:'Braucht schon unser eigenes Schema: der <b>per-Ordner-Accent</b> (<code>type: folder</code>) und die Farbe einer <code>select</code>-Option — letztere wird im Typ-Editor gewählt, nicht in einen Record getippt. Darum vorerst <b>intern</b>; später in die Palette heben ist billig. Kanonisch <code>#rrggbb</code>.',
|
||||
pill:'✅', cte:'✅', idx:'text'},
|
||||
|
||||
{g:'scalars', id:'select', name:'Select / MultiSelect / Tags', icon:I.list, mods:['options','multiple','open','default'],
|
||||
yaml:`<span class="k">status:</span> offen <span class="c"># eigenes Feld: Wert verbatim</span>
|
||||
<span class="k">genres:</span> [Drama, SciFi] <span class="c"># multiple: true</span>
|
||||
<span class="k">tags:</span> [wandern, 2026] <span class="c"># open: true</span>`,
|
||||
view:`<div class="stack">
|
||||
<span class="kv"><span class="k">Status</span><span class="v">Offen</span></span>
|
||||
<div class="chiprow"><span class="kv"><span class="v">Drama</span></span><span class="kv"><span class="v">SciFi</span></span></div>
|
||||
<div class="chiprow"><span class="kv"><span class="colorpill" style="background:#3E7CB1"></span><span class="v">wandern</span></span><span class="kv"><span class="colorpill" style="background:#9B5DE5"></span><span class="v">2026</span></span></div></div>`,
|
||||
edit:`<div class="stack">
|
||||
<span class="ipill act"><span class="k">Status</span><span class="v ivsel">Offen</span></span>
|
||||
`+opts(['Offen','In Arbeit','Erledigt','Abgebrochen'],'Offen')+`
|
||||
<div class="tags" style="max-width:280px"><span class="tagchip">wandern <button id="tg1"></button></span><span class="tagchip">2026 <button id="tg2"></button></span><input placeholder="Tag …"></div></div>`,
|
||||
note:'<b>Gespeichert wird der Wert, nicht eine Options-ID</b> — die Datei bleibt ohne Schema lesbar; Umbenennen ist eine explizite Migration. Im <b>Basisschema</b> ist das Standard-Token der Wert (<code>NEEDS-ACTION</code>), weil die Projektion es so braucht. <code>select</code> ist auch die <b>Kanban-Achse</b>.',
|
||||
pill:'✅', cte:'✅', idx:'text · Kanban-Achse'},
|
||||
|
||||
/* ---------------- B · references ---------------- */
|
||||
{g:'refs', id:'link', name:'Verknüpfung (ObjectRef)', icon:I.link, mods:['to','multiple'],
|
||||
yaml:`<span class="k">project:</span> "[[a1b2c3]]" <span class="c"># immer quoten!</span>
|
||||
<span class="k">director:</span> "[[9f8e7d]]" <span class="c"># to: contact</span>`,
|
||||
view:`<div class="stack">
|
||||
<span class="kv"><span class="k">Projekt</span><span class="v"><a class="wlink">Hausumbau</a></span></span>
|
||||
<div class="panelframe">`+prow(I.link,'Regie','<a class="wlink">Ridley Scott</a>')+`</div></div>`,
|
||||
edit:`<div class="stack">
|
||||
<span class="ipill act"><span class="k">Projekt</span><span class="v ivsel">Hausumb</span></span>
|
||||
<span class="ivpop"><span class="ivlist">
|
||||
<button class="ivopt sel">`+I.home+` Hausumbau<span class="ivnow">aktuell</span></button>
|
||||
<button class="ivopt">`+I.text+` Hausumbau — Budget</button>
|
||||
<button class="ivopt">`+I.pin+` Haus Zürich</button>
|
||||
</span></span></div>`,
|
||||
note:'Speichert die <b>Wikilink-Form</b> — eine Link-Notation in Body <i>und</i> Frontmatter, immer über die stabile <code>id</code>. <b>Unquoted wäre <code>[[a1b2c3]]</code> eine verschachtelte YAML-Liste.</b> <code>to:</code> macht daraus eine benannte Kante im Graph.',
|
||||
pill:'✅', cte:'✅', idx:'Kante (from, feld, to)'},
|
||||
|
||||
{g:'refs', id:'url', name:'URL', icon:I.globe, mods:['multiple'],
|
||||
yaml:`<span class="k">homepage:</span> https://terrata.example`,
|
||||
view:`<div class="stack">
|
||||
<span class="kv"><span class="k">Website</span><span class="v"><a class="wlink">terrata.example</a></span></span>
|
||||
<div class="panelframe">`+prow(I.globe,'Website','<a class="wlink">terrata.example</a>')+`</div></div>`,
|
||||
edit:`<div class="stack">
|
||||
<span class="ipill act"><span class="k">Website</span><span class="v ivsel">https://terrata.example</span></span>
|
||||
`+fld('Website','<input class="finput" value="https://terrata.example">')+`
|
||||
<button class="btn-ghost">`+I.globe+` Öffnen</button></div>`,
|
||||
note:'Eigener Typ statt <code>text</code>+Format wegen Icon, Tastatur, <b>Aktion</b> (öffnen) und vCard-<code>URL</code>. Anzeige gekürzt, gespeichert vollständig.',
|
||||
pill:'✅', cte:'✅', idx:'text'},
|
||||
|
||||
{g:'refs', id:'email', name:'E-Mail', icon:I.mail, mods:['multiple'],
|
||||
yaml:`<span class="k">contact_mail:</span> info@restaurant.ch
|
||||
<span class="c"># kein label, keine Reihenfolge-Priorität —</span>
|
||||
<span class="c"># beides gibt es nur bei contact (vCard TYPE)</span>`,
|
||||
view:`<div class="stack">
|
||||
<span class="kv"><span class="k">E-Mail</span><span class="v">info@restaurant.ch</span></span>
|
||||
<div class="panelframe">`+prow(I.mail,'E-Mail','info@restaurant.ch')+`</div></div>`,
|
||||
edit:`<div class="stack">
|
||||
<span class="ipill act"><span class="k">E-Mail</span><span class="v ivsel">info@restaurant.ch</span></span>
|
||||
`+fld('E-Mail',inp('info@restaurant.ch'))+`
|
||||
<button class="btn-ghost">`+I.mail+` Mail schreiben</button></div>`,
|
||||
note:'Als Palette-Feld ein <b>blosser Wert</b> (mit <code>multiple</code> eine flache Liste) — das ist die konkrete Antwort auf „E-Mail ohne Label". Eigener Typ statt <code>text</code>+Format wegen Icon, Tastatur und <b>Aktion</b> (mailto).',
|
||||
pill:'✅', cte:'✅', idx:'text · FTS'},
|
||||
|
||||
{g:'refs', id:'tel', name:'Telefon', icon:I.phone, mods:['multiple'],
|
||||
yaml:`<span class="k">hotline:</span> "+41 44 000 00 00" <span class="c"># quoten (+, 0)</span>`,
|
||||
view:`<div class="stack">
|
||||
<span class="kv"><span class="k">Hotline</span><span class="v">+41 44 000 00 00</span></span>
|
||||
<div class="panelframe">`+prow(I.phone,'Hotline','+41 44 000 00 00')+`</div></div>`,
|
||||
edit:`<div class="stack">
|
||||
<span class="ipill act"><span class="k">Hotline</span><span class="v ivsel">+41 44 000 00 00</span></span>
|
||||
`+fld('Hotline',inp('+41 44 000 00 00'))+`</div>`,
|
||||
note:'Wird <b>verbatim so gespeichert, wie getippt</b>; eine Normalisierung auf eine <code>tel:</code>-URI passiert nur in der vCard-Projektion (also nur bei <code>contact</code>). Ohne Quotes verliert YAML das <code>+</code> bzw. führende Nullen.',
|
||||
pill:'✅', cte:'✅', idx:'text'},
|
||||
|
||||
{g:'refs', id:'file', name:'Datei', icon:I.file, mods:['accept','multiple'],
|
||||
yaml:`<span class="k">menu:</span> menu.pdf <span class="c"># eigenes Asset, relativ</span>
|
||||
<span class="k">plan:</span> "[[9f8e7d/plan.pdf]]" <span class="c"># fremdes Bundle</span>`,
|
||||
view:`<div class="stack">
|
||||
<span class="filechip"><span class="fi">`+I.file+`</span> menu.pdf <span class="rm">`+I.x+`</span></span>
|
||||
<div class="panelframe">`+prow(I.file,'Plan','plan.pdf')+`</div></div>`,
|
||||
edit:`<div class="stack">
|
||||
<span class="filechip"><span class="fi">`+I.file+`</span> menu.pdf <span class="rm">`+I.x+`</span></span>
|
||||
<div class="dropzone">`+I.upload+`<div style="margin-top:6px">Datei hierher ziehen oder wählen</div></div></div>`,
|
||||
note:'Kein eigener „Image"-Typ: ein Titelbild ist <code>{ type: file, accept: "image/*" }</code>. Spiegelt die Embed-Regeln (eigenes Asset relativ, fremdes als <code>[[<id>/name]]</code>).',
|
||||
pill:'✅ (Chip)', cte:'Bleistift', idx:'text (Existenz)'},
|
||||
|
||||
/* ---------------- D · geo ---------------- */
|
||||
{g:'geo', id:'geo', name:'Koordinate', icon:I.pin, mods:['multiple'],
|
||||
yaml:`<span class="k">geo:</span>
|
||||
<span class="k">lat:</span> 46.6892 <span class="c"># benannt, nie positional</span>
|
||||
<span class="k">lon:</span> 7.8506
|
||||
<span class="k">alt:</span> 1322 <span class="c"># optional</span>`,
|
||||
view:`<div class="stack">
|
||||
<div class="chiprow"><span class="kv"><span class="k">Koordinate</span><span class="v">46.6892, 7.8506</span></span><span class="kv"><span class="k">Höhe</span><span class="v">1322 m ü. M.</span></span></div>
|
||||
<div class="minimap"><span class="pin"></span></div></div>`,
|
||||
edit:`<div class="stack">
|
||||
<div class="frow">`+fld('Breite',inp('46.6892'))+fld('Länge',inp('7.8506'))+fld('Höhe',inp('1322'))+`</div>
|
||||
<div class="minimap"><span class="pin"></span></div>
|
||||
<button class="btn-ghost">`+I.crosshair+` Aus Adresse ableiten</button></div>`,
|
||||
note:'<b>Feldtyp</b> <code>geo</code> ≠ <b>Record-Typ</b> <code>location</code>: ein Record kann mehrere benannte Koordinatenfelder haben (<code>geo</code>, <code>summit</code>, <code>parking</code>). Pill nur kompakt; der Klick führt zum Block, nicht in einen Editor. R-Tree-Punkt.',
|
||||
pill:'kompakt', cte:'Bleistift', idx:'R-Tree-Punkt'},
|
||||
|
||||
{g:'geo', id:'address', name:'Adresse', icon:I.home, mods:[],
|
||||
yaml:`<span class="k">address:</span> <span class="c"># einwertig, ohne Label</span>
|
||||
<span class="k">street:</span> Bahnhofstrasse 1
|
||||
<span class="k">postcode:</span> "8001" <span class="c"># quoten (führende 0)</span>
|
||||
<span class="k">locality:</span> Zürich
|
||||
<span class="k">country:</span> CH
|
||||
<span class="k">geo:</span> <span class="c"># Cache, aufgelöst aus</span>
|
||||
<span class="k">lat:</span> 47.3702 <span class="c"># den Teilen darüber —</span>
|
||||
<span class="k">lon:</span> 8.5401 <span class="c"># die Position sagt es</span>`,
|
||||
view:`<div class="stack">
|
||||
<div class="panelframe" style="gap:8px">`+prow(I.home,'Adresse','Bahnhofstrasse 1<br>8001 Zürich')+`
|
||||
<div class="minimap" style="max-width:100%;height:78px"><span class="pin"></span></div></div>
|
||||
<span style="font-size:11.5px;color:var(--muted)">Adresse + Karte = <b>eine</b> Einheit, weil sie im Frontmatter eine sind</span></div>`,
|
||||
edit:`<div class="stack">
|
||||
`+fld('Strasse',inp('Bahnhofstrasse 1'))+`
|
||||
<div class="frow">`+fld('PLZ','<input class="finput" style="width:80px" value="8001">')+fld('Ort',inp('Zürich'))+`</div>
|
||||
<button class="btn-ghost">`+I.crosshair+` Koordinaten auflösen</button>
|
||||
<div class="minimap"><span class="pin"></span></div>
|
||||
<span class="derived">`+I.sparkles+`<span><b>47.3702, 8.5401</b> — <i>aufgelöst; Pin verschieben setzt <code>manual: true</code></i></span></span></div>`,
|
||||
note:'Der <b>einzige Palette-Typ mit verschachteltem Wert</b> — die Ausnahme lohnt, weil erst sie einen <i>Ort</i> per Name eingebbar macht (eigener Typ <code>restaurant</code>). Der Cache steht <b>in</b> der Adresse: das macht Getipptes/Berechnetes ohne Flag erkennbar und beides zu <b>einer</b> Render-Einheit. Und er steht in der <b>Datei</b>, weil der Index pro Gerät neu gebaut und nicht synchronisiert wird — bei einem <b>rate-limitierten Gratis-Dienst</b> ist das der Unterschied zwischen einer Abfrage pro Adresse pro <i>Vault</i> und einer pro <i>Gerät</i>. Aufgelöst wird nur auf <b>explizite Aktion</b>, nie beim Rendern.',
|
||||
pill:'—', cte:'Bleistift', idx:'Text + R-Tree via geo'},
|
||||
|
||||
{g:'geo', id:'track', name:'Track', icon:I.track, mods:['accept'],
|
||||
yaml:`<span class="k">track:</span>
|
||||
<span class="k">file:</span> hardergrat.gpx <span class="c"># Quelle, autoritativ</span>
|
||||
<span class="k">bbox:</span> [7.85, 46.68, 7.92, 46.73]
|
||||
<span class="k">distance_m:</span> 24300 <span class="c"># Cache, daraus</span>
|
||||
<span class="k">ascent_m:</span> 1650 <span class="c"># berechnet</span>`,
|
||||
view:`<div class="stack">
|
||||
<div class="chiprow"><span class="kv"><span class="k">Länge</span><span class="v">24,3 km</span></span><span class="kv"><span class="k">Auf/Ab</span><span class="v">↑1650 ↓1480 m</span></span></div>
|
||||
<div class="minimap"><svg class="route" viewBox="0 0 230 96" preserveAspectRatio="none"><path d="M18 78 C60 70 60 34 100 30 S170 44 212 18"/></svg></div></div>`,
|
||||
edit:`<div class="stack">
|
||||
<span class="filechip"><span class="fi">`+I.file+`</span> hardergrat.gpx <span class="rm">`+I.x+`</span></span>
|
||||
<span class="derived">`+I.sparkles+`<span><b>24,3 km</b> · ↑1650 · ↓1480 — <i>aus GPX abgeleitet</i></span></span>
|
||||
<div class="dropzone">`+I.upload+`<div style="margin-top:6px">GPX ersetzen</div></div></div>`,
|
||||
note:'<b>Dasselbe Muster wie bei der Adresse</b>: der Cache liegt im Objekt seiner Quelle, hier neben <code>file</code>. Die Geometriedatei ist die Wahrheit; Länge/Auf/Ab sind <b>read-only berechnet</b> — auch im Feld-Edit-Zustand, denn der Weg zu anderen Werten ist ein anderes GPX. Der Gewinn: man liest „24,3 km", ohne 4213 Trackpoints zu parsen. R-Tree indexiert die <code>bbox</code>. Start/Ziel-Adressen sind der reverse-geocodierte Fall derselben Regel.',
|
||||
pill:'—', cte:'Bleistift', idx:'R-Tree-bbox + Summary'},
|
||||
|
||||
/* ---------------- D · base schema only ---------------- */
|
||||
{g:'std', id:'labeled', name:'Labeled Multi-Values (tel · email · adr)', icon:I.phone, mods:['labeled','multiple'],
|
||||
yaml:`<span class="k">tel:</span>
|
||||
- <span class="k">label:</span> Mobil
|
||||
<span class="k">value:</span> "+41 79 000 00 00"
|
||||
- <span class="k">label:</span> Arbeit
|
||||
<span class="k">value:</span> "+41 44 000 00 00"`,
|
||||
view:`<div class="stack">
|
||||
<span class="kv"><span class="k">Mobil</span><span class="v">+41 79 000 00 00</span></span>
|
||||
<div class="panelframe">`+prow(I.phone,'Telefon','+41 79 000 00 00','Mobil · primär')+prow(I.phone,'Telefon','+41 44 000 00 00','Arbeit')+`</div></div>`,
|
||||
edit:`<div class="rlist">
|
||||
<div class="rrow"><span class="grip">`+I.grip+`</span><input class="finput rlabel" value="Mobil"><input class="finput rval" value="+41 79 000 00 00"><button class="rrem">`+I.x+`</button></div>
|
||||
<div class="rrow"><span class="grip">`+I.grip+`</span><input class="finput rlabel" value="Arbeit"><input class="finput rval" value="+41 44 000 00 00"><button class="rrem">`+I.x+`</button></div>
|
||||
<button class="addrow">`+I.plus+` hinzufügen</button></div>`,
|
||||
note:'<b>Der Grund, warum der Kontakt-Editor Griffe hat</b> — und der Grund, warum die Palette sie <i>nicht</i> braucht: Labels existieren, weil vCard <code>TYPE</code>-Parameter kennt. <b>Reihenfolge = Priorität</b> (oberste Zeile primär) statt eines <code>PREF</code>-Sterns; Labels bleiben <b>verbatim</b>, die Abbildung auf <code>TYPE=work</code> / <code>X-ABLabel</code> passiert erst in der Projektion. Bei <code>adr</code> ist der <code>value</code> selbst ein Objekt (ADR-Teile); jeder Eintrag darf ein eigenes <code>geo</code> tragen — geschrieben auf explizite Aktion, nicht als Cache.',
|
||||
pill:'✅ (pro Eintrag)', cte:'Bleistift', idx:'text · FTS'},
|
||||
|
||||
{g:'std', id:'recurrence', name:'Wiederholung', icon:I.repeat, mods:[],
|
||||
yaml:`<span class="k">rrule:</span> "FREQ=WEEKLY;BYDAY=TU" <span class="c"># verbatim</span>
|
||||
<span class="k">exdate:</span> [2026-09-01]`,
|
||||
view:`<div class="stack">
|
||||
<span class="kv"><span class="k">Wiederholung</span><span class="v">wöchentlich, Di</span></span>
|
||||
<span style="font-size:11.5px;color:var(--muted)">1 Ausnahme (1. Sep 2026)</span></div>`,
|
||||
edit:`<div class="stack">
|
||||
`+fld('Wiederholung','<select class="finput"><option>Keine</option><option>Täglich</option><option selected>Wöchentlich</option><option>Monatlich</option><option>Jährlich</option><option>Benutzerdefiniert …</option></select>')+`
|
||||
<span style="font-size:11.5px;color:var(--muted)">Voller Muster-Editor (Ausnahmen, „dieser / alle / folgende") ist bewusst später</span></div>`,
|
||||
note:'<b>Die RRULE bleibt als String stehen.</b> Ein YAML-Re-Encoding eines bereits kompakten Standards ist eine verlustanfällige Übersetzung — und die UI ist ohnehin ein Preset-Select.',
|
||||
pill:'Zusammenfassung', cte:'Bleistift', idx:'text'},
|
||||
|
||||
{g:'std', id:'reminder', name:'Erinnerung', icon:I.bell, mods:['multiple'],
|
||||
yaml:`<span class="k">alarms:</span>
|
||||
- <span class="k">trigger:</span> -PT15M <span class="c"># relativ zu start/due</span>
|
||||
<span class="k">action:</span> display
|
||||
- <span class="k">trigger:</span> -P1D`,
|
||||
view:`<div class="stack">
|
||||
<div class="panelframe">`+prow(I.bell,'Erinnerung','15 Minuten vor Beginn')+prow(I.bell,'Erinnerung','1 Tag vor Beginn')+`</div></div>`,
|
||||
edit:`<div class="rlist">
|
||||
<div class="rrow"><span class="grip">`+I.grip+`</span><input class="finput" style="width:56px" value="15"><select class="finput" style="width:96px"><option selected>Minuten</option><option>Stunden</option><option>Tage</option></select><select class="finput"><option selected>vor Beginn</option><option>nach Beginn</option></select><button class="rrem">`+I.x+`</button></div>
|
||||
<button class="addrow">`+I.plus+` Erinnerung</button></div>`,
|
||||
note:'<code>VALARM</code> ist Sub-Komponente von <b>VEVENT und VTODO</b> — dieselbe Komponente gilt für Termine und Aufgaben. Repeatable; <code>trigger</code> ist ein <code>duration</code>-Wert (oder absolut).',
|
||||
pill:'—', cte:'Bleistift', idx:'text (Existenz)'},
|
||||
|
||||
{g:'std', id:'attendee', name:'Teilnehmer', icon:I.users, mods:['multiple'],
|
||||
yaml:`<span class="k">attendees:</span>
|
||||
- <span class="k">name:</span> Anna Muster
|
||||
<span class="k">email:</span> anna@example.com
|
||||
<span class="k">ref:</span> "[[a1b2c3]]" <span class="c"># optional: Kontakt</span>
|
||||
<span class="k">status:</span> accepted`,
|
||||
view:`<div class="stack">
|
||||
<div class="chiprow"><span class="kv"><span class="colorpill" style="background:var(--edge-out)"></span><span class="v">Anna Muster</span></span><span class="kv"><span class="v">ben@example.com</span></span></div></div>`,
|
||||
edit:`<div class="stack">
|
||||
<div class="tags" style="max-width:290px"><span class="tagchip">Anna Muster <button id="at1"></button></span><span class="tagchip">ben@example.com <button id="at2"></button></span><input placeholder="Teilnehmer …"></div>
|
||||
<span style="font-size:11.5px;color:var(--muted)">Autocomplete über Kontakte — freie Adresse bleibt erlaubt</span></div>`,
|
||||
note:'<b>Darf</b> auf einen Kontakt zeigen (<code>ref</code>), <b>muss</b> aber nicht: eine Einladung darf nicht davon abhängen, dass die Person zuerst als Kontakt-Bundle existiert.',
|
||||
pill:'—', cte:'Bleistift', idx:'text · FTS'},
|
||||
|
||||
{g:'std', id:'codec', name:'Codecs (Priorität) & TZID', icon:I.hash, mods:['readonly'],
|
||||
yaml:`<span class="k">priority:</span> 5 <span class="c"># VTODO 0–9, gespeichert</span>
|
||||
<span class="k">tz:</span> Europe/Zurich <span class="c"># TZID für start/end</span>`,
|
||||
view:`<div class="stack">
|
||||
<span class="kv"><span class="k">Priorität</span><span class="v">Mittel</span></span>
|
||||
<span style="font-size:11.5px;color:var(--muted)">0 → Keine · 1–4 → Hoch · 5 → Mittel · 6–9 → Niedrig</span></div>`,
|
||||
edit:`<div class="stack">
|
||||
<span class="ipill act"><span class="k">Priorität</span><span class="v ivsel">Mittel</span></span>
|
||||
`+opts(['Keine','Niedrig','Mittel','Hoch'],'Mittel')+`</div>`,
|
||||
note:'<b>Ein Codec ist kein Typ.</b> Gespeichert wird, was der Standard fordert (Integer 0–9), <i>angezeigt</i> werden drei Stufen — und eine <code>3</code> aus einem Fremdclient bleibt <code>3</code>, statt auf <code>1</code> normiert zu werden. Ebenso <code>tz</code>: nur <code>event</code> zahlt für <code>TZID</code>, statt jeden Datumswert in ein Objekt zu wickeln.',
|
||||
pill:'✅', cte:'✅', idx:'numerisch'},
|
||||
|
||||
/* ---------------- E · fallback ---------------- */
|
||||
{g:'raw', id:'raw', name:'Nicht modelliert', icon:I.braces, mods:[],
|
||||
yaml:`<span class="k">imported_from:</span> notion <span class="c"># kein Schema-Feld</span>
|
||||
<span class="k">weird:</span>
|
||||
- { a: 1, b: [2, 3] } <span class="c"># Form passt zu keinem Typ</span>`,
|
||||
view:`<div class="stack">
|
||||
<div class="panelframe">
|
||||
`+prow(I.braces,'imported_from','notion')+`
|
||||
<div><div class="rolbl">nicht modelliert — unverändert übernommen</div><div class="ro">weird:<br> - { a: 1, b: [2, 3] }</div></div>
|
||||
</div>
|
||||
<span class="warn">`+I.warn+` Wert liegt ausserhalb von <code>min/max</code> — nur ein Hinweis</span></div>`,
|
||||
edit:`<div class="stack">
|
||||
`+fld('imported_from',inp('notion'))+`
|
||||
<div><div class="rolbl">read-only</div><div class="ro">weird: [ { a: 1, b: [2, 3] } ]</div></div>
|
||||
<span style="font-size:11.5px;color:var(--muted)">Auch der Feld-Edit-Zustand macht daraus keinen Editor</span></div>`,
|
||||
note:'Das Frontmatter-Gegenstück zum <b>Opaque-Block</b>: Skalare werden nach erkannter Art editierbar, Listen/Objekte bleiben <b>verbatim und read-only</b>. Genau das macht „eine handgeschriebene Datei wird nie abgelehnt" wahr statt bloss gut gemeint. Validierung <b>informiert, korrigiert nie</b>.',
|
||||
pill:'—', cte:'nur Skalare', idx:'nicht typisiert'},
|
||||
];
|
||||
|
||||
/* ---------- render cards ---------- */
|
||||
/* the base-schema group reads best with the labeled multi-values first */
|
||||
const BASE_ORDER = ['labeled','duration','recurrence','reminder','attendee','color','codec'];
|
||||
const itemsOf = gid => gid === 'std'
|
||||
? T.filter(t => t.g === gid).sort((a,b) => BASE_ORDER.indexOf(a.id) - BASE_ORDER.indexOf(b.id))
|
||||
: T.filter(t => t.g === gid);
|
||||
const badge = s => s === 'pal'
|
||||
? '<span class="bscope pal">Palette</span>'
|
||||
: '<span class="bscope base">nur Basisschema</span>';
|
||||
const cards = GROUPS.map(g => {
|
||||
const items = itemsOf(g.id).map(t => `
|
||||
<div class="demo ftcard" id="t-${t.id}">
|
||||
<div class="fthead">
|
||||
<span class="ti">${t.icon}</span>
|
||||
<span class="tid">${t.id}</span>
|
||||
<span class="tnm">${t.name}</span>
|
||||
${badge(g.scope)}
|
||||
<span class="tmods">${t.mods.map(m => '<span class="modchip">'+m+'</span>').join('')}</span>
|
||||
</div>
|
||||
<div class="ftgrid">
|
||||
<div class="ftcol"><div class="ftlbl">Frontmatter</div><pre class="yaml">${t.yaml}</pre></div>
|
||||
<div class="ftcol"><div class="ftlbl">Ansicht — Pill / Panel-Row</div>${t.view}</div>
|
||||
<div class="ftcol"><div class="ftlbl">Bearbeiten — klick-direkt / Field-Editor</div>${t.edit}</div>
|
||||
</div>
|
||||
<div class="ftnote">${t.note}</div>
|
||||
</div>`).join('');
|
||||
return `<section id="${g.id}"><h2>${g.title}</h2><p class="desc">${g.desc}</p>${items}</section>`;
|
||||
}).join('');
|
||||
document.getElementById('cards').innerHTML = cards;
|
||||
|
||||
/* ---------- render nav ---------- */
|
||||
document.getElementById('nav').insertAdjacentHTML('beforeend', GROUPS.map(g =>
|
||||
`<h4>${g.title}</h4>` + itemsOf(g.id).map(t =>
|
||||
`<a href="#t-${t.id}">${t.icon}<span>${t.name}</span><code>${t.id}</code></a>`).join('')).join(''));
|
||||
|
||||
/* ---------- render matrix ---------- */
|
||||
const mark = v => v === '✅' ? '<span class="yes">✅</span>' : v === '—' ? '<span class="no">—</span>' : '<span class="no">'+v+'</span>';
|
||||
document.getElementById('mxtable').innerHTML =
|
||||
'<thead><tr><th>Typ</th><th>Katalog</th><th>Pill</th><th>Klick-direkt</th><th>Index</th><th>Modifikatoren</th></tr></thead><tbody>' +
|
||||
GROUPS.map(g => '<tr class="grp"><td colspan="6">'+g.title+'</td></tr>' + itemsOf(g.id).map(t =>
|
||||
`<tr><td class="t">${t.id}<div style="font-family:var(--font-body);color:var(--muted);font-size:11.5px">${t.name}</div></td>
|
||||
<td class="c">${badge(g.scope)}</td>
|
||||
<td class="c">${mark(t.pill)}</td><td class="c">${mark(t.cte)}</td>
|
||||
<td>${t.idx}</td><td style="color:var(--muted);font-family:var(--font-mono);font-size:11px">${t.mods.join(' · ') || '—'}</td></tr>`).join('')).join('') +
|
||||
'</tbody>';
|
||||
|
||||
/* ---------- deviations: the five kinds + their offered repairs ---------- */
|
||||
const DEV = [
|
||||
{k:'Typ passt nicht', w:'konflikt', icon:I.warn,
|
||||
t:'<b>due</b> ist als <code>date</code> deklariert, der Wert ist keines',
|
||||
v:'due: "Das ist definitiv kein Datum"',
|
||||
why:'Der einzige Fall, der wirklich etwas kostet: der Wert ist nicht typisierbar — kein Sortieren, kein Index, kein typisierter Editor.',
|
||||
fix:[['Interpretieren','prim'],['Als Text behalten','schema'],['Wert löschen',''],['Belassen','']]},
|
||||
{k:'Nicht deklariert', w:'', icon:I.braces,
|
||||
t:'<b>mood</b> steht in der Datei, aber nicht im Typ',
|
||||
v:'mood: gut',
|
||||
why:'Öfter legitim als nicht: Import, anderes Tool, oder ein Feld, das <i>zuerst</i> in die Datei getippt und später deklariert wird.',
|
||||
fix:[['Als Feld übernehmen','schema'],['Umbenennen auf „due“','schema'],['Belassen','']]},
|
||||
{k:'Fehlt', w:'', icon:I.list,
|
||||
t:'<b>due</b> ist deklariert, in der Datei nicht vorhanden',
|
||||
v:'# (kein Key — genau das ist „nicht gesetzt“)',
|
||||
why:'<b>Keine</b> Abweichung, solange das Feld nicht <code>required</code> ist: „fehlt = nicht gesetzt“ ist der Normalfall, leere optionale Felder sind ausgeblendet, nicht markiert.',
|
||||
fix:[['nur bei required: Feld ausfüllen',''],['Nicht mehr erforderlich','schema']]},
|
||||
{k:'Regel verletzt', w:'', icon:I.hash,
|
||||
t:'<b>rating</b> ist 15, das Schema sagt <code>max: 10</code>',
|
||||
v:'rating: 15',
|
||||
why:'Der Wert ist brauchbar — er ist nur nicht erlaubt. Also Hinweis, kein Konflikt.',
|
||||
fix:[['Wert anpassen','prim'],['Regel anpassen','schema'],['Belassen','']]},
|
||||
{k:'Typdefinition fehlt', w:'', icon:I.unknown,
|
||||
t:'<code>type: movie</code>, aber (noch) kein <code>movie</code>-Schema hier',
|
||||
v:'type: movie # alle Felder rendern als raw',
|
||||
why:'Schemas reisen als Bundles — das ist der normale Zustand <i>während</i> eines Syncs, kein kaputter Record. Ein Hinweis am Record, nicht 40 an den Feldern.',
|
||||
fix:[['nichts reparierbar — die echte Definition kommt per Sync','']]},
|
||||
];
|
||||
document.getElementById('devlist').innerHTML = DEV.map(d => `
|
||||
<div class="dev ${d.w}">
|
||||
<span class="di">${d.icon}</span>
|
||||
<div class="dc">
|
||||
<div class="dk">${d.k} · ${d.w === 'konflikt' ? 'Konflikt' : 'Hinweis'}</div>
|
||||
<div class="dt">${d.t}</div>
|
||||
<div class="dv">${d.v}</div>
|
||||
<div class="dt" style="color:var(--muted);margin-top:6px">${d.why}</div>
|
||||
<div class="fixrow">${d.fix.map(f =>
|
||||
`<button class="fix ${f[1]}">${f[0]}${f[1] === 'schema' ? '<span class="sc">Typdefinition</span>' : ''}</button>`).join('')}</div>
|
||||
</div>
|
||||
</div>`).join('') +
|
||||
`<div class="ftnote" style="margin:6px -18px -18px"><b>Interpretieren</b> zeigt immer
|
||||
<span class="arrowto">"14.9.2026" → <b>2026-09-14</b></span> und wird bestätigt — bei mehrdeutigen Werten
|
||||
werden <b>beide Lesarten angeboten</b> statt eine geraten. <b>Sammel-Reparatur</b> gibt es mit denselben
|
||||
Aktionen von der <i>Typdefinition</i> her („in N Records anwenden“, mit Vorschauliste) — dort sitzt meist die
|
||||
Ursache. MVP: Anzeige + Reparatur pro Record; Sammel-Reparatur mit Vorschau ist Phase 2.</div>`;
|
||||
|
||||
/* ---------- render the palette menu ("+ Eigenschaft") + the comparison ---------- */
|
||||
document.getElementById('palette').innerHTML =
|
||||
'<div class="ph">' + I.plus + ' Eigenschaft hinzufügen</div>' +
|
||||
GROUPS.filter(g => g.scope === 'pal').map(g =>
|
||||
itemsOf(g.id).map(t =>
|
||||
`<button class="pmi">${t.icon}<span>${t.name}</span><code>${t.id}</code></button>`).join('')).join('');
|
||||
|
||||
const CMP = [
|
||||
['Wer deklariert', 'der <b>Benutzer</b> (schema-Bundle / „+ Eigenschaft")', '<b>wir</b>, im Code, pro eingebautem Typ'],
|
||||
['Getrieben von', 'was jemand festhalten will', 'vCard / iCalendar'],
|
||||
['Komplexität', '<b>bewusst einfach</b> — ein Wert, evtl. mehrere', 'was der Standard fordert'],
|
||||
['Projektion', '<code>X-TERRATA-*</code>, nie eine Standard-Property', 'die Standard-Property selbst'],
|
||||
['Bearbeiten', '<b>immer klick-direkt</b>, kein Modus', 'meist Feld-Edit-Zustand'],
|
||||
];
|
||||
document.getElementById('cmp').innerHTML =
|
||||
'<thead><tr><th></th><th>Palette</th><th>Basisschema</th></tr></thead><tbody>' +
|
||||
CMP.map(r => `<tr><td style="color:var(--muted);white-space:nowrap">${r[0]}</td><td>${r[1]}</td><td>${r[2]}</td></tr>`).join('') +
|
||||
'</tbody>';
|
||||
|
||||
/* ---------- fill the remaining static icons ---------- */
|
||||
['mx1','mx2','ax2','ax4','tg1','tg2','at1','at2'].forEach(id => set(id, I.x));
|
||||
['ax1','ax3'].forEach(id => set(id, I.grip));
|
||||
set('ax5', I.plus + ' hinzufügen');
|
||||
set('cmpicon', I.sparkles);
|
||||
['dm1','dp1','dp2'].forEach(id => set(id, I.warn));
|
||||
set('dp3', I.braces);
|
||||
set('cb1', I.warn + ' 1');
|
||||
set('hi1', I.info); set('hi2', I.link);
|
||||
set('hardicon', I.sparkles);
|
||||
|
||||
/* ---------- theme toggle ---------- */
|
||||
function isDark(){
|
||||
const t=document.documentElement.getAttribute('data-theme');
|
||||
if(t) return t==='dark';
|
||||
return matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
function applyThemeIcon(){
|
||||
set('themeicon', isDark()?I.sun:I.moon);
|
||||
document.getElementById('themetxt').textContent = isDark()?'Hell':'Dunkel';
|
||||
}
|
||||
document.getElementById('themebtn').onclick=()=>{
|
||||
const cur=document.documentElement.getAttribute('data-theme');
|
||||
const next = cur? (cur==='dark'?'light':'dark') : (matchMedia('(prefers-color-scheme: dark)').matches?'light':'dark');
|
||||
document.documentElement.setAttribute('data-theme',next);
|
||||
reapplyAccent(); applyThemeIcon();
|
||||
};
|
||||
|
||||
/* ---------- accent switcher (per-folder demo) ---------- */
|
||||
document.querySelectorAll('.swatchbtn').forEach(b=>{
|
||||
b.onclick=()=>{
|
||||
document.querySelectorAll('.swatchbtn').forEach(x=>x.classList.remove('sel'));
|
||||
b.classList.add('sel');
|
||||
document.documentElement.dataset.accentL=b.dataset.accent;
|
||||
document.documentElement.dataset.accentD=b.dataset.accentD;
|
||||
reapplyAccent();
|
||||
};
|
||||
});
|
||||
function reapplyAccent(){
|
||||
const l=document.documentElement.dataset.accentL, d=document.documentElement.dataset.accentD;
|
||||
if(l&&d) document.documentElement.style.setProperty('--accent', isDark()?d:l);
|
||||
}
|
||||
matchMedia('(prefers-color-scheme: dark)').addEventListener('change',()=>{
|
||||
if(!document.documentElement.getAttribute('data-theme')){reapplyAccent();applyThemeIcon();}
|
||||
});
|
||||
|
||||
applyThemeIcon();
|
||||
</script>
|
||||