From 81ebcd66a16ac629cbdbeaf2d2e3375f099e6c39 Mon Sep 17 00:00:00 2001 From: SRHTNIN Date: Tue, 13 Oct 2026 16:35:21 +0200 Subject: [PATCH] Properties: colours by type, readable keys, lines between Co-Authored-By: Claude Opus 5.5 --- Client/Properties.py | 64 ++++++++++++++++++++++++++++++----- Frontend/src/Properties.js | 25 ++++++++++++-- Frontend/src/Styles/Theme.css | 26 ++++++++++++-- 3 files changed, 101 insertions(+), 14 deletions(-) diff --git a/Client/Properties.py b/Client/Properties.py index 774fa47..c1925c6 100644 --- a/Client/Properties.py +++ b/Client/Properties.py @@ -23,6 +23,37 @@ OpenOrClose = re.compile(r"^---\s*$") PropertyLine = re.compile(r"^([^:\s][^:]*?)\s*:\s*(.*)$") TaskLine = re.compile(r"^(\s*(?:[-*+]|\d+[.)])\s+)\[( |x|X)\]") BooleanValue = re.compile(r"^(true|false)$", re.IGNORECASE) +Quoted = re.compile(r"""^(["']).*\1$""") +IntegerValue = re.compile(r"^[+-]?\d+$") +FloatValue = re.compile(r"^[+-]?(\d+\.\d*([eE][+-]?\d+)?|\.\d+([eE][+-]?\d+)?|\d+[eE][+-]?\d+)$") +DateValue = re.compile(r"^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?)?$") + + +def ValueKind(Value: str) -> str: + """What a value is, for its colour: quotes always make it text ("42" is + text, 42 a number), like YAML. Same rules as Properties.js's ValueKind.""" + if Value == "": + return "Empty" + if Quoted.match(Value): + return "String" + if BooleanValue.match(Value): + return "Boolean" + if IntegerValue.match(Value): + return "Integer" + if FloatValue.match(Value): + return "Float" + if DateValue.match(Value): + return "Date" + return "String" + + +def ShownValue(Value: str) -> str: + """A value as shown: without the quotes that only said "this is text".""" + return Value[1:-1] if Quoted.match(Value) else Value + + +def KindColour(Kind: str) -> str: + return {"Integer": Palette.Peach, "Float": Palette.Yellow, "Date": Palette.Blue}.get(Kind, Palette.Green) # Ticks a box: called with the line (0-based, in the whole note) to flip. OnToggle = Optional[Callable[[int], None]] @@ -93,26 +124,41 @@ def _Checkbox(Checked: bool, Line: int, Toggle: OnToggle) -> ft.Checkbox: ) +def _Value(Value: str, FontSize: int, MakeMarkdown: Callable[[str], ft.Control]) -> ft.Control: + """A value in its kind's colour; one with Markdown in it (a link, bold) + is rendered as Markdown instead.""" + Shown = ShownValue(Value) + if re.search(r"[\[*_`~]|https?://", Shown): + return MakeMarkdown(Shown) + return ft.Text(Shown, color=KindColour(ValueKind(Value)), size=FontSize, selectable=True) + + def PropertiesView(Found: Parsed, FontSize: int, MakeMarkdown: Callable[[str], ft.Control], Toggle: OnToggle) -> ft.Control: - """The panel. MakeMarkdown renders a value (links, bold).""" + """The panel. MakeMarkdown renders a value with Markdown in it (links, bold).""" Rows: list[ft.Control] = [] for Item in Found.Properties: if BooleanValue.match(Item.Value): Shown: ft.Control = _Checkbox(Item.Value.lower() == "true", Item.Line, Toggle) elif Item.List: - Shown = ft.Row([ft.Container(ft.Text(Entry, color=Palette.Mauve, size=FontSize - 1), bgcolor=Palette.Surface0, - border_radius=4, padding=ft.Padding.symmetric(horizontal=6, vertical=1)) + Shown = ft.Row([ft.Container(ft.Text(ShownValue(Entry), color=KindColour(ValueKind(Entry)), size=FontSize - 1), + bgcolor=Palette.Surface0, border_radius=4, + padding=ft.Padding.symmetric(horizontal=6, vertical=1)) for Entry in Item.List], wrap=True, spacing=4, run_spacing=4) elif Item.Value: - Shown = MakeMarkdown(Item.Value) + Shown = _Value(Item.Value, FontSize, MakeMarkdown) else: Shown = ft.Text("Empty", color=Palette.Overlay0, italic=True, size=FontSize - 1) - Rows.append(ft.Row([ - ft.Container(ft.Text(Item.Key, color=Palette.Overlay0, size=FontSize - 1, max_lines=1, - overflow=ft.TextOverflow.ELLIPSIS), expand=3), - ft.Container(Shown, expand=7, alignment=ft.Alignment.CENTER_LEFT), - ], spacing=12, vertical_alignment=ft.CrossAxisAlignment.CENTER)) + Rows.append(ft.Container( + ft.Row([ + ft.Container(ft.Text(Item.Key, color=Palette.Text, size=FontSize - 1, max_lines=1, + overflow=ft.TextOverflow.ELLIPSIS), expand=3), + ft.Container(Shown, expand=7, alignment=ft.Alignment.CENTER_LEFT), + ], spacing=12, vertical_alignment=ft.CrossAxisAlignment.CENTER), + # A line between properties, like the web view's. + border=ft.Border.only(top=ft.BorderSide(1, Palette.Surface1)) if Rows else None, + padding=ft.Padding.symmetric(vertical=4), + )) return ft.Container( ft.Column([ft.Text("Properties", color=Palette.Overlay0, size=FontSize - 1), *Rows], spacing=2, tight=True), bgcolor=Palette.Mantle, diff --git a/Frontend/src/Properties.js b/Frontend/src/Properties.js index 3b24e7b..bc78ab8 100644 --- a/Frontend/src/Properties.js +++ b/Frontend/src/Properties.js @@ -46,6 +46,23 @@ export function IsBoolean(Value) { return /^(true|false)$/i.test(Value); } +// What a value is, for its colour: quotes always make it text ("42" is text, +// 42 a number), like YAML. Same rules as the client's Properties.ValueKind. +export function ValueKind(Value) { + if (Value === "") return "Empty"; + if (/^(["']).*\1$/.test(Value)) return "String"; + if (IsBoolean(Value)) return "Boolean"; + if (/^[+-]?\d+$/.test(Value)) return "Integer"; + if (/^[+-]?(\d+\.\d*([eE][+-]?\d+)?|\.\d+([eE][+-]?\d+)?|\d+[eE][+-]?\d+)$/.test(Value)) return "Float"; + if (/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?)?$/.test(Value)) return "Date"; + return "String"; +} + +// A value as shown: without the quotes that only said "this is text". +export function ShownValue(Value) { + return /^(["']).*\1$/.test(Value) ? Value.slice(1, -1) : Value; +} + // The note without its properties block (what Markdown renders). export function WithoutProperties(Text) { const Parsed = ParseProperties(Text); @@ -99,9 +116,13 @@ export function PropertiesHtml(Parsed, RenderInline, Escape, CanEdit) { const Disabled = CanEdit ? "" : " disabled"; Shown = ``; } else if (List) { - Shown = List.map((Item) => `${RenderInline(Item)}`).join(""); + Shown = List.map( + (Item) => `${RenderInline(ShownValue(Item))}`, + ).join(""); + } else if (Value) { + Shown = `${RenderInline(ShownValue(Value))}`; } else { - Shown = Value ? RenderInline(Value) : `Empty`; + Shown = `Empty`; } return `
${Escape(Key)}${Shown}
`; }); diff --git a/Frontend/src/Styles/Theme.css b/Frontend/src/Styles/Theme.css index 6d37199..83324a2 100644 --- a/Frontend/src/Styles/Theme.css +++ b/Frontend/src/Styles/Theme.css @@ -527,7 +527,11 @@ img, display: flex; align-items: center; gap: 12px; - padding: 2px 0; + padding: 4px 0; +} + +.PropertyRow + .PropertyRow { + border-top: 1px solid var(--Border); } .PropertyKey { @@ -536,7 +540,7 @@ img, overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - color: var(--TextMuted); + color: var(--Text); } .PropertyValue { @@ -552,7 +556,23 @@ img, padding: 0 6px; border-radius: 4px; background: var(--SurfaceAlt); - color: var(--Purple); +} + +/* Values by kind (Properties.js ValueKind): text, whole and decimal numbers, dates. */ +.PropertyKindString { + color: var(--Green); +} + +.PropertyKindInteger { + color: var(--Orange); +} + +.PropertyKindFloat { + color: var(--Yellow); +} + +.PropertyKindDate { + color: var(--Blue); } .PropertyEmpty {