Files
Gotcha/crates/app/ui/components.slint
2026-07-30 18:39:26 +02:00

319 lines
16 KiB
Plaintext

import { Palette } from "std-widgets.slint";
import { CommentRow, FileRow, MarkdownBlock } from "types.slint";
export global IconTheme {
in-out property <int> style: 0;
}
export global AppTheme {
in-out property <int> mode: 0;
private property <color> system-background: Palette.background;
out property <bool> dark: mode == 2 || (mode == 0 && system-background.to-oklch().lightness < 0.5);
out property <color> accent: dark ? #0a84ff : #0879e1;
out property <color> accent-soft: dark ? #17324d : #e5f2fd;
out property <color> page: dark ? #1c1c1e : #f2f2f7;
out property <color> surface: dark ? #2c2c2e : #ffffff;
out property <color> surface-muted: dark ? #3a3a3c : #f5f5f8;
out property <color> header: dark ? #242426 : #f8f8fa;
out property <color> toolbar: dark ? #242426 : #fbfbfc;
out property <color> border: dark ? #48484a : #dedee3;
out property <color> separator: dark ? #3a3a3c : #d8d8dd;
out property <color> text: dark ? #f2f2f7 : #17171a;
out property <color> text-secondary: dark ? #c7c7cc : #5f5f66;
out property <color> text-tertiary: dark ? #8e8e93 : #85858c;
out property <color> code-background: dark ? #242426 : #e9e9ed;
out property <color> quote-background: dark ? #242426 : #f6f6f8;
out property <color> loading-overlay: dark ? #00000088 : #ffffff88;
changed mode => {
Palette.color-scheme = mode == 1 ? ColorScheme.light : mode == 2 ? ColorScheme.dark : ColorScheme.unknown;
}
}
export component AppIcon inherits Rectangle {
in property <string> kind;
in property <int> style: IconTheme.style;
in property <color> icon-color: AppTheme.accent;
background: transparent;
private property <bool> duotone: root.style == 1;
private property <bool> bold: root.style == 2;
private property <bool> issue-kind: root.kind == "issues" || root.kind == "issue-open" || root.kind == "issue-close" || root.kind == "issue-reopen" || root.kind == "issue-comment";
private property <bool> repository-kind: root.kind == "repositories" || root.kind == "repo-create" || root.kind == "repo-rename" || root.kind == "repo-star" || root.kind == "repo-watch" || root.kind == "update";
private property <bool> pull-kind: root.kind == "pulls" || root.kind == "pull-open" || root.kind == "pull-merge" || root.kind == "pull-close" || root.kind == "pull-reopen" || root.kind == "pull-comment" || root.kind == "pull-approve" || root.kind == "pull-reject";
private property <bool> tag-kind: root.kind == "tag-push" || root.kind == "tag-delete";
private property <bool> branch-kind: root.kind == "branch-create" || root.kind == "branch-delete";
private property <bool> closed-shape: root.kind == "home" || root.issue-kind || root.repository-kind || root.kind == "favorite" || root.kind == "favorite-filled" || root.tag-kind || root.kind == "release";
private property <bool> has-badge: root.kind == "branch-create" || root.kind == "branch-delete" || root.kind == "push" || root.kind == "repo-create" || root.kind == "repo-rename" || root.kind == "repo-star" || root.kind == "repo-watch" || root.kind == "update" || root.kind == "issue-open" || root.kind == "issue-close" || root.kind == "issue-reopen" || root.kind == "issue-comment" || root.kind == "pull-open" || root.kind == "pull-merge" || root.kind == "pull-close" || root.kind == "pull-reopen" || root.kind == "pull-comment" || root.kind == "pull-approve" || root.kind == "pull-reject" || root.kind == "tag-push" || root.kind == "tag-delete";
private property <bool> positive: root.kind == "issue-close" || root.kind == "pull-merge" || root.kind == "pull-approve";
private property <bool> destructive: root.kind == "branch-delete" || root.kind == "pull-close" || root.kind == "pull-reject" || root.kind == "tag-delete";
private property <color> badge-color: root.positive ? #20a866 : root.destructive ? #e45b4d : root.duotone ? #26bfd1 : root.icon-color;
private property <string> shape: root.kind == "home"
? "M3 10.5 L12 3 L21 10.5 V21 H15 V14 H9 V21 H3 Z"
: root.issue-kind
? "M4 5 H20 V9 C18 9 18 15 20 15 V19 H4 V15 C6 15 6 9 4 9 Z"
: root.repository-kind
? "M6 3 H18 C19.1 3 20 3.9 20 5 V21 H6 C4.9 21 4 20.1 4 19 V5 C4 3.9 4.9 3 6 3 Z"
: root.pull-kind
? "M7 4 V20 M17 4 V11 C17 15 14 17 10 17 M7 4 H7 M17 4 H17"
: root.kind == "settings"
? "M12 3 V6 M12 18 V21 M3 12 H6 M18 12 H21 M5.6 5.6 L7.8 7.8 M16.2 16.2 L18.4 18.4 M18.4 5.6 L16.2 7.8 M7.8 16.2 L5.6 18.4"
: root.kind == "back"
? (root.bold ? "M16 4 L7 12 L16 20 Z" : "M16 4 L7 12 L16 20")
: root.kind == "filter"
? (root.bold ? "M3 5 H21 L14 13 V20 L10 22 V13 Z" : "M4 6 H20 M4 12 H20 M4 18 H20")
: root.kind == "favorite" || root.kind == "favorite-filled"
? "M12 3 L14.8 8.7 L21 9.6 L16.5 14 L17.6 20.3 L12 17.3 L6.4 20.3 L7.5 14 L3 9.6 L9.2 8.7 Z"
: root.kind == "disclosure"
? (root.bold ? "M8 4 L17 12 L8 20 Z" : "M8 4 L17 12 L8 20")
: root.branch-kind
? "M7 4 V20 M7 11 C11 11 14 9 14 5"
: root.kind == "push"
? "M4 8 H13 M4 12 H13 M4 16 H13"
: root.tag-kind
? "M3 5 V13 L11 21 L21 11 L13 3 H5 C3.9 3 3 3.9 3 5 Z"
: root.kind == "release"
? "M4 8 L12 4 L20 8 V18 L12 22 L4 18 Z"
: "M12 4 V20 M4 12 H20";
private property <string> detail: root.kind == "home"
? "M9 21 V14 H15 V21"
: root.issue-kind
? "M12 8 V13 M12 16.5 V17"
: root.repository-kind
? "M7 3 V21 M11 9 L8.5 12 L11 15 M15 9 L17.5 12 L15 15"
: root.pull-kind
? "M7 4 A1.8 1.8 0 1 1 6.99 4 M7 20 A1.8 1.8 0 1 1 6.99 20 M17 4 A1.8 1.8 0 1 1 16.99 4"
: root.kind == "settings"
? "M12 8 A4 4 0 1 1 11.99 8"
: root.kind == "filter" && !root.bold
? "M9 4 V8 M15 10 V14 M8 16 V20"
: root.tag-kind
? "M8 8 A1.5 1.5 0 1 1 7.99 8"
: root.kind == "release"
? "M12 18 V8 M9 11 L12 8 L15 11 M7 6 V3 M17 6 V3"
: "";
private property <string> badge-detail: root.kind == "branch-create" || root.kind == "repo-create" || root.kind == "issue-open" || root.kind == "pull-open"
? "M18 15.5 V20.5 M15.5 18 H20.5"
: root.kind == "issue-close" || root.kind == "pull-merge" || root.kind == "pull-approve"
? "M15.4 18 L17.2 19.8 L20.7 16.2"
: root.kind == "branch-delete" || root.kind == "tag-delete"
? "M15.3 18 H20.7"
: root.kind == "pull-close" || root.kind == "pull-reject"
? "M16 16 L20 20 M20 16 L16 20"
: root.kind == "push" || root.kind == "tag-push"
? "M18 20.5 V15.5 M15.7 17.8 L18 15.5 L20.3 17.8"
: root.kind == "repo-rename"
? "M15.3 20.6 L16 18 L19.7 14.3 L21.7 16.3 L18 20 Z"
: root.kind == "repo-star"
? "M18 14.5 L19 16.7 L21.4 17 L19.6 18.7 L20.1 21 L18 19.8 L15.9 21 L16.4 18.7 L14.6 17 L17 16.7 Z"
: root.kind == "repo-watch"
? "M14.5 18 C16.3 15.8 19.7 15.8 21.5 18 C19.7 20.2 16.3 20.2 14.5 18 M18 17 A1 1 0 1 1 17.99 17"
: root.kind == "issue-reopen" || root.kind == "pull-reopen"
? "M20.8 17 A3 3 0 1 0 20.2 20.3 M20.8 17 H18.5 M20.8 17 V19.3"
: root.kind == "issue-comment" || root.kind == "pull-comment"
? "M15.5 17 H20.5 V20 H18 L16 21 V20 H15.5 Z M17 18.5 H17.1 M19 18.5 H19.1"
: root.kind == "update"
? "M18 14.8 L18.8 17.2 L21.2 18 L18.8 18.8 L18 21.2 L17.2 18.8 L14.8 18 L17.2 17.2 Z"
: "";
Path {
width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24;
commands: root.shape;
fill: root.closed-shape ? (root.bold || root.kind == "favorite-filled" ? root.icon-color : root.duotone ? #26bfd133 : transparent) : transparent;
stroke: root.closed-shape && (root.bold || root.kind == "favorite-filled") ? transparent : root.icon-color;
stroke-width: root.bold ? 3px : root.duotone ? 2.2px : 1.8px;
stroke-line-cap: round; stroke-line-join: round;
}
if root.detail != "": Path {
width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24;
commands: root.detail; fill: transparent;
stroke: root.bold && root.closed-shape ? #ffffff : root.icon-color;
stroke-width: root.bold ? 2.4px : 1.8px;
stroke-line-cap: round; stroke-line-join: round;
}
if root.has-badge: Path {
width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24;
commands: "M18 13.5 A4.5 4.5 0 1 1 17.99 13.5 Z";
fill: root.style == 0 ? AppTheme.surface : root.badge-color;
stroke: root.style == 0 ? root.badge-color : #ffffff;
stroke-width: root.style == 2 ? 1.5px : 1.2px;
}
if root.has-badge: Path {
width: 100%; height: 100%; viewbox-width: 24; viewbox-height: 24;
commands: root.badge-detail; fill: root.style == 0 ? transparent : #ffffff;
stroke: root.style == 0 ? root.badge-color : #ffffff;
stroke-width: root.bold ? 2px : 1.6px;
stroke-line-cap: round; stroke-line-join: round;
}
}
export component IconStyleChoice inherits Rectangle {
in property <int> value;
in property <string> label;
callback clicked();
background: IconTheme.style == root.value ? AppTheme.accent-soft : AppTheme.surface-muted;
border-color: IconTheme.style == root.value ? AppTheme.accent : AppTheme.border;
border-width: IconTheme.style == root.value ? 2px : 1px;
border-radius: 11px;
TouchArea { clicked => { root.clicked(); } }
AppIcon { x: (parent.width - 30px) / 2; y: 10px; width: 30px; height: 30px; kind: "repositories"; style: root.value; }
Text { y: 48px; width: 100%; text: root.label; font-size: 12px; font-weight: IconTheme.style == root.value ? 650 : 500; color: AppTheme.text; horizontal-alignment: center; }
}
export component AppearanceChoice inherits Rectangle {
in property <int> value;
in property <string> label;
callback clicked();
background: AppTheme.mode == root.value ? AppTheme.accent-soft : AppTheme.surface-muted;
border-color: AppTheme.mode == root.value ? AppTheme.accent : AppTheme.border;
border-width: AppTheme.mode == root.value ? 2px : 1px;
border-radius: 11px;
TouchArea { clicked => { root.clicked(); } }
Text {
width: 100%; height: 100%; text: root.label;
font-size: 14px; font-weight: AppTheme.mode == root.value ? 650 : 500;
color: AppTheme.mode == root.value ? AppTheme.accent : AppTheme.text;
horizontal-alignment: center; vertical-alignment: center;
}
}
export component Header inherits Rectangle {
in property <string> title;
in property <bool> can_back;
in property <bool> can_filter: false;
callback back();
callback filter();
height: 58px;
width: 100%;
background: AppTheme.header;
border-color: AppTheme.border;
border-width: 0px;
if can_back: TouchArea {
x: 8px; width: 54px;
clicked => { root.back(); }
AppIcon { x: 7px; y: 17px; width: 24px; height: 24px; kind: "back"; }
}
Text {
x: root.can_back || root.can_filter ? 62px : 12px;
width: parent.width - 2 * self.x;
text: root.title;
horizontal-alignment: center;
vertical-alignment: center;
font-size: 18px;
font-weight: 600;
color: AppTheme.text;
overflow: elide;
}
if can_filter: TouchArea {
x: parent.width - 58px; width: 58px;
clicked => { root.filter(); }
AppIcon { x: 17px; y: 17px; width: 24px; height: 24px; kind: "filter"; }
}
Rectangle { y: parent.height - 1px; height: 1px; background: AppTheme.border; }
}
export component EdgeBack inherits SwipeGestureHandler {
callback back();
width: 24px;
height: 100%;
handle-swipe-right: true;
swiped => { root.back(); }
}
export component EmptyState inherits VerticalLayout {
in property <string> title;
in property <string> detail;
alignment: center;
spacing: 8px;
Text { text: root.title; font-size: 21px; font-weight: 600; horizontal-alignment: center; color: AppTheme.text; }
Text { text: root.detail; font-size: 15px; horizontal-alignment: center; wrap: word-wrap; color: AppTheme.text-secondary; }
}
export component MarkdownContent inherits VerticalLayout {
in property <[MarkdownBlock]> blocks;
spacing: 8px;
for block in root.blocks: Rectangle {
height: block.kind == "rule" ? 9px : rendered.preferred-height + (block.kind == "code" || block.kind == "table" ? 16px : 0px);
background: block.kind == "code" || block.kind == "table" ? AppTheme.code-background : block.kind == "quote" ? AppTheme.quote-background : transparent;
border-radius: block.kind == "code" || block.kind == "table" ? 7px : 0px;
if block.kind == "quote": Rectangle { width: 3px; height: 100%; background: AppTheme.text-tertiary; border-radius: 2px; }
if block.kind == "rule": Rectangle { y: 4px; width: 100%; height: 1px; background: AppTheme.separator; }
rendered := StyledText {
x: block.kind == "code" || block.kind == "table" ? 8px : block.kind == "quote" ? 11px : 0px;
y: block.kind == "code" || block.kind == "table" ? 8px : 0px;
width: parent.width - self.x - (block.kind == "code" || block.kind == "table" ? 8px : 0px);
text: block.text;
default-color: AppTheme.text;
link-color: AppTheme.accent;
default-font-family: block.kind == "code" || block.kind == "table" ? "monospace" : "";
default-font-size: block.kind == "heading1" ? 24px : block.kind == "heading2" ? 21px : block.kind == "heading3" ? 19px : block.kind == "heading4" ? 17px : 16px;
}
}
}
export component CommentCard inherits Rectangle {
in property <CommentRow> comment;
height: content.preferred-height + 24px;
background: AppTheme.surface;
border-radius: 10px;
content := VerticalLayout {
x: 12px; y: 10px; width: parent.width - 24px;
spacing: 5px;
Text { text: root.comment.author; font-size: 13px; font-weight: 600; color: AppTheme.text; }
MarkdownContent { width: parent.width; blocks: root.comment.body; }
Text { text: root.comment.meta; font-size: 11px; color: AppTheme.text-tertiary; }
}
}
export component FileCard inherits Rectangle {
in property <FileRow> file;
callback clicked();
height: 66px;
background: AppTheme.surface;
border-color: AppTheme.border;
border-width: 1px;
border-radius: 10px;
TouchArea { clicked => { root.clicked(); } }
Text { x: 13px; y: 10px; width: parent.width - 26px; text: root.file.path; font-size: 14px; font-weight: 600; color: AppTheme.text; overflow: elide; }
Text { x: 13px; y: 38px; width: parent.width - 26px; text: root.file.status; font-size: 12px; color: AppTheme.text-secondary; }
}
export component TabButton inherits Rectangle {
in property <string> kind;
in property <string> label;
in property <bool> active;
private property <length> icon-area-height: root.height * 3 / 4;
private property <length> icon-size: min(42px, self.icon-area-height - 6px);
callback clicked();
background: transparent;
TouchArea { clicked => { root.clicked(); } }
AppIcon {
x: (parent.width - root.icon-size) / 2;
y: (root.icon-area-height - root.icon-size) / 2;
width: root.icon-size; height: root.icon-size;
kind: root.kind; icon-color: root.active ? AppTheme.accent : AppTheme.text-tertiary;
}
Text {
y: root.icon-area-height; height: parent.height - self.y; width: 100%; text: root.label;
color: root.active ? AppTheme.accent : AppTheme.text-tertiary; font-size: 9px;
font-weight: root.active ? 600 : 400;
horizontal-alignment: center; vertical-alignment: center;
}
}
export component PullToRefreshGesture inherits SwipeGestureHandler {
in property <bool> at_top;
in property <bool> refreshing;
callback refresh();
enabled: root.at_top && !root.refreshing;
handle-swipe-down: true;
swiped => { root.refresh(); }
@children
if self.swiping: Rectangle {
x: (parent.width - 142px) / 2; y: 8px; width: 142px; height: 32px;
background: AppTheme.surface-muted; border-radius: 16px;
Text { text: "Release to refresh"; font-size: 12px; color: AppTheme.text-secondary; horizontal-alignment: center; vertical-alignment: center; }
}
}