feat: implement client backend

This includes temporary AI-generated frontend scaffolding, none of this will be used in production. It was just used during development for the backend.
This commit is contained in:
2026-10-09 19:46:02 +01:00
parent 88a64768a2
commit 5eb47e45ef
49 changed files with 3576 additions and 17 deletions
+155
View File
@@ -0,0 +1,155 @@
/// TEMPORARY AI GENERATED FRONTEND \\\
/// NOT TO BE USED IN PRODUCTION \\\
import { Theme } from "../theme.slint";
import { Icons } from "../icons.slint";
import { Icon } from "icon.slint";
import { ToolButton } from "tool-button.slint";
// Message box. Enter sends, Shift+Enter inserts a newline (same as Discord).
// The component is deliberately stateless about *what* is being sent: the
// parent owns the reply/edit state and just tells us what banner to show.
export component Composer inherits Rectangle {
in property <string> placeholder;
in property <string> banner; // "Replying to Alice" / "Editing message"; "" hides it
in property <bool> editing;
in property <string> typing-text; // "Bob is typing…"
callback submit(string);
callback cancel-banner();
callback clear-text();
callback typing();
callback attach();
// Parent -> composer requests, as properties so the composer can live inside an `if`.
in property <string> text-to-set;
in property <int> text-nonce; // bump to replace the input text with `text-to-set` (and focus)
in property <int> focus-nonce; // bump to just focus the input
changed text-nonce => {
input.text = root.text-to-set;
input.focus();
}
changed focus-nonce => {
input.focus();
}
// Grows with content up to ~8 lines, like Discord.
property <length> input-height: min(max(input.preferred-height, 24px), 180px);
height: (root.banner != "" ? 32px : 0px) + input-height + 24px + 24px;
background: Theme.chat-bg;
// Reply / edit banner sits above the input and shares its rounded top.
if root.banner != "": Rectangle {
x: 16px;
y: 0px;
width: parent.width - 32px;
height: 32px + 8px;
background: Theme.sidebar-bg;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
Text {
x: 12px;
width: parent.width - 60px;
height: 32px;
text: root.banner;
color: Theme.text-muted;
font-size: 13px;
vertical-alignment: center;
overflow: elide;
}
ToolButton {
glyph: Icons.close;
x: parent.width - self.width - 6px;
y: 2px;
clicked => {
root.cancel-banner();
}
}
}
box := Rectangle {
x: 16px;
y: (root.banner != "" ? 32px : 0px);
width: parent.width - 32px;
height: root.input-height + 24px;
background: Theme.input-bg;
border-radius: root.banner != "" ? 0px : 8px;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
ToolButton {
glyph: Icons.plus-circle;
x: 6px;
y: parent.height - self.height - 10px;
clicked => {
root.attach();
}
}
FocusScope {
x: 44px;
y: 12px;
width: parent.width - 44px - 52px;
height: root.input-height;
// Intercept Enter before the TextInput turns it into a newline.
capture-key-pressed(event) => {
if (event.text == Key.Return && !event.modifiers.shift) {
if (input.text != "") {
root.submit(input.text);
input.text = "";
}
return accept;
}
return reject;
}
input := TextInput {
width: parent.width;
height: parent.height;
single-line: false;
wrap: word-wrap;
color: Theme.text-normal;
font-size: 15px;
selection-background-color: Theme.accent;
edited => {
root.typing();
}
}
}
if input.text == "": Text {
x: 44px;
y: 12px;
width: parent.width - 100px;
height: 24px;
text: root.placeholder;
color: Theme.text-faint;
font-size: 15px;
vertical-alignment: center;
overflow: elide;
}
ToolButton {
glyph: Icons.smile;
x: parent.width - self.width - 8px;
y: parent.height - self.height - 10px;
// TODO: emoji picker
}
}
// Typing indicator, under the box.
Text {
x: 20px;
y: parent.height - 22px;
width: parent.width - 40px;
height: 18px;
text: root.typing-text;
color: Theme.text-muted;
font-size: 12px;
font-weight: 600;
vertical-alignment: center;
overflow: elide;
}
}