-
This commit is contained in:
116
ui/components/accordion.slint
Normal file
116
ui/components/accordion.slint
Normal file
@@ -0,0 +1,116 @@
|
||||
import { Theme, SpacingSystem, Typography, Animations } from "../theme/theme.slint";
|
||||
|
||||
export struct AccordionItemData {
|
||||
title: string,
|
||||
expanded: bool,
|
||||
}
|
||||
|
||||
export component Accordion {
|
||||
in-out property <[AccordionItemData]> items: [];
|
||||
in property <bool> allow-multiple: false;
|
||||
|
||||
callback item-toggled(int, bool);
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 0;
|
||||
|
||||
for item[index] in root.items: AccordionItem {
|
||||
title: item.title;
|
||||
expanded: item.expanded;
|
||||
is-last: index == root.items.length - 1;
|
||||
|
||||
toggled => {
|
||||
root.item-toggled(index, !item.expanded);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component AccordionItem {
|
||||
in property <string> title: "";
|
||||
in property <bool> expanded: false;
|
||||
in property <bool> is-last: false;
|
||||
|
||||
callback toggled();
|
||||
|
||||
private property <bool> hovered: false;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 0;
|
||||
|
||||
// Header
|
||||
header := Rectangle {
|
||||
height: 48px;
|
||||
background: root.hovered ? Theme.colors.muted : transparent;
|
||||
|
||||
animate background { duration: Animations.durations.fast; }
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: SpacingSystem.spacing.s4;
|
||||
padding-right: SpacingSystem.spacing.s4;
|
||||
spacing: SpacingSystem.spacing.s4;
|
||||
alignment: space-between;
|
||||
|
||||
Text {
|
||||
text: root.title;
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
font-weight: Typography.weights.medium;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
chevron := Text {
|
||||
text: "›";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.lg;
|
||||
font-weight: Typography.weights.bold;
|
||||
vertical-alignment: center;
|
||||
width: 20px;
|
||||
horizontal-alignment: center;
|
||||
|
||||
rotation-angle: root.expanded ? 90deg : 0deg;
|
||||
rotation-origin-x: self.width / 2;
|
||||
rotation-origin-y: self.height / 2;
|
||||
|
||||
animate rotation-angle { duration: Animations.durations.normal; easing: Animations.ease-out; }
|
||||
}
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
clicked => {
|
||||
root.toggled();
|
||||
}
|
||||
|
||||
moved => {
|
||||
root.hovered = self.has-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
if root.expanded: content-wrapper := Rectangle {
|
||||
background: transparent;
|
||||
|
||||
VerticalLayout {
|
||||
padding: SpacingSystem.spacing.s4;
|
||||
padding-top: 0;
|
||||
padding-bottom: SpacingSystem.spacing.s4;
|
||||
|
||||
@children
|
||||
}
|
||||
}
|
||||
|
||||
// Border
|
||||
if !root.is-last: Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.colors.border;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for accordion content
|
||||
export component AccordionContent {
|
||||
VerticalLayout {
|
||||
@children
|
||||
}
|
||||
}
|
||||
238
ui/components/app-sidebar.slint
Normal file
238
ui/components/app-sidebar.slint
Normal file
@@ -0,0 +1,238 @@
|
||||
import { Theme, SpacingSystem, Typography, Animations } from "../theme/theme.slint";
|
||||
import { Button } from "./button.slint";
|
||||
import { StatusIndicator, StatusState } from "./status-indicator.slint";
|
||||
|
||||
export struct NavItem {
|
||||
title: string,
|
||||
icon: string,
|
||||
}
|
||||
|
||||
export component AppSidebar {
|
||||
in property <[NavItem]> nav-items: [];
|
||||
in-out property <int> current-page: 0;
|
||||
in property <StatusState> mihomo-status: StatusState.stopped;
|
||||
in property <bool> mihomo-loading: false;
|
||||
|
||||
callback navigate(int);
|
||||
callback toggle-mihomo();
|
||||
|
||||
width: 240px;
|
||||
|
||||
Rectangle {
|
||||
background: Theme.colors.background;
|
||||
border-width: 1px;
|
||||
border-color: Theme.colors.border;
|
||||
|
||||
VerticalLayout {
|
||||
padding: SpacingSystem.spacing.s4;
|
||||
spacing: SpacingSystem.spacing.s4;
|
||||
|
||||
// Header
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
|
||||
Rectangle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: SpacingSystem.radius.md;
|
||||
background: Theme.colors.primary;
|
||||
|
||||
Text {
|
||||
text: "C";
|
||||
color: Theme.colors.primary-foreground;
|
||||
font-size: Typography.sizes.lg;
|
||||
font-weight: Typography.weights.bold;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 2px;
|
||||
|
||||
Text {
|
||||
text: "Clash Manager";
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
font-weight: Typography.weights.semibold;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "v1.0.0";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
Text {
|
||||
text: "NAVIGATION";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: 10px;
|
||||
font-weight: Typography.weights.semibold;
|
||||
}
|
||||
|
||||
for item[index] in root.nav-items: NavButton {
|
||||
icon: item.icon;
|
||||
title: item.title;
|
||||
active: index == root.current-page;
|
||||
|
||||
clicked => {
|
||||
root.navigate(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
vertical-stretch: 1;
|
||||
}
|
||||
|
||||
// Mihomo Status Footer
|
||||
MihomoStatusBar {
|
||||
status: root.mihomo-status;
|
||||
loading: root.mihomo-loading;
|
||||
|
||||
toggle-clicked => {
|
||||
root.toggle-mihomo();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component NavButton {
|
||||
in property <string> icon: "";
|
||||
in property <string> title: "";
|
||||
in property <bool> active: false;
|
||||
|
||||
callback clicked();
|
||||
|
||||
private property <bool> hovered: false;
|
||||
|
||||
height: 40px;
|
||||
|
||||
states [
|
||||
active when root.active: {
|
||||
container.background: Theme.colors.primary.transparentize(0.9);
|
||||
icon-text.color: Theme.colors.primary;
|
||||
title-text.color: Theme.colors.foreground;
|
||||
}
|
||||
hovered when root.hovered && !root.active: {
|
||||
container.background: Theme.colors.muted.transparentize(0.5);
|
||||
}
|
||||
]
|
||||
|
||||
container := Rectangle {
|
||||
background: transparent;
|
||||
border-radius: SpacingSystem.radius.md;
|
||||
|
||||
animate background { duration: Animations.durations.fast; }
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: SpacingSystem.spacing.s3;
|
||||
padding-right: SpacingSystem.spacing.s3;
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
|
||||
icon-text := Text {
|
||||
text: root.icon;
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.lg;
|
||||
vertical-alignment: center;
|
||||
width: 20px;
|
||||
|
||||
animate color { duration: Animations.durations.fast; }
|
||||
}
|
||||
|
||||
title-text := Text {
|
||||
text: root.title;
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
font-weight: Typography.weights.medium;
|
||||
vertical-alignment: center;
|
||||
|
||||
animate color { duration: Animations.durations.fast; }
|
||||
}
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
clicked => {
|
||||
root.clicked();
|
||||
}
|
||||
|
||||
moved => {
|
||||
root.hovered = self.has-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component MihomoStatusBar {
|
||||
in property <StatusState> status: StatusState.stopped;
|
||||
in property <bool> loading: false;
|
||||
|
||||
callback toggle-clicked();
|
||||
|
||||
private property <string> status-text: status == StatusState.running ? "Running" :
|
||||
status == StatusState.starting ? "Starting..." :
|
||||
"Stopped";
|
||||
|
||||
private property <color> status-color: status == StatusState.running ? #22c55e :
|
||||
status == StatusState.starting ? #eab308 :
|
||||
#9ca3af;
|
||||
|
||||
Rectangle {
|
||||
background: status == StatusState.starting ? #eab30820 :
|
||||
status == StatusState.running ? #22c55e20 :
|
||||
transparent;
|
||||
border-radius: SpacingSystem.radius.md;
|
||||
|
||||
animate background { duration: Animations.durations.normal; }
|
||||
|
||||
HorizontalLayout {
|
||||
padding: SpacingSystem.spacing.s3;
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
alignment: space-between;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpacingSystem.spacing.s3;
|
||||
|
||||
StatusIndicator {
|
||||
state: root.status;
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 2px;
|
||||
|
||||
Text {
|
||||
text: "Clash";
|
||||
color: root.status-color;
|
||||
font-size: Typography.sizes.xs;
|
||||
font-weight: Typography.weights.semibold;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.status-text;
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
text: root.loading ? "..." : (status == StatusState.running ? "||" : ">");
|
||||
variant: "ghost";
|
||||
disabled: root.loading;
|
||||
|
||||
clicked => {
|
||||
root.toggle-clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
ui/components/label.slint
Normal file
44
ui/components/label.slint
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Theme, Typography } from "../theme/theme.slint";
|
||||
|
||||
export enum LabelSize {
|
||||
xs,
|
||||
sm,
|
||||
base,
|
||||
lg,
|
||||
xl,
|
||||
}
|
||||
|
||||
export enum LabelColor {
|
||||
foreground,
|
||||
muted,
|
||||
primary,
|
||||
secondary,
|
||||
destructive,
|
||||
}
|
||||
|
||||
export component Label {
|
||||
in property <string> text: "";
|
||||
in property <LabelSize> size: LabelSize.base;
|
||||
in property <LabelColor> color-variant: LabelColor.foreground;
|
||||
in property <int> font-weight: Typography.weights.normal;
|
||||
|
||||
private property <length> font-size: size == LabelSize.xs ? Typography.sizes.xs :
|
||||
size == LabelSize.sm ? Typography.sizes.sm :
|
||||
size == LabelSize.lg ? Typography.sizes.lg :
|
||||
size == LabelSize.xl ? Typography.sizes.xl :
|
||||
Typography.sizes.base;
|
||||
|
||||
private property <color> text-color: color-variant == LabelColor.muted ? Theme.colors.muted-foreground :
|
||||
color-variant == LabelColor.primary ? Theme.colors.primary :
|
||||
color-variant == LabelColor.secondary ? Theme.colors.secondary :
|
||||
color-variant == LabelColor.destructive ? Theme.colors.destructive :
|
||||
Theme.colors.foreground;
|
||||
|
||||
Text {
|
||||
text: root.text;
|
||||
color: root.text-color;
|
||||
font-size: root.font-size;
|
||||
font-weight: root.font-weight;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
36
ui/components/progress.slint
Normal file
36
ui/components/progress.slint
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Theme, SpacingSystem, Typography, Animations } from "../theme/theme.slint";
|
||||
|
||||
export component Progress {
|
||||
in property <float> value: 0; // 0-100
|
||||
in property <bool> show-label: false;
|
||||
|
||||
min-height: 8px;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s1;
|
||||
|
||||
// Progress bar
|
||||
track := Rectangle {
|
||||
height: 8px;
|
||||
background: Theme.colors.primary.transparentize(0.8);
|
||||
border-radius: SpacingSystem.radius.full;
|
||||
|
||||
indicator := Rectangle {
|
||||
width: parent.width * Math.max(0, Math.min(100, root.value)) / 100;
|
||||
height: parent.height;
|
||||
background: Theme.colors.primary;
|
||||
border-radius: SpacingSystem.radius.full;
|
||||
|
||||
animate width { duration: Animations.durations.normal; easing: Animations.ease-out; }
|
||||
}
|
||||
}
|
||||
|
||||
// Optional label
|
||||
if root.show-label: label := Text {
|
||||
text: Math.round(root.value) + "%";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.xs;
|
||||
horizontal-alignment: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
174
ui/components/select.slint
Normal file
174
ui/components/select.slint
Normal file
@@ -0,0 +1,174 @@
|
||||
import { Theme, SpacingSystem, Typography, Animations } from "../theme/theme.slint";
|
||||
|
||||
export struct SelectOption {
|
||||
label: string,
|
||||
value: string,
|
||||
}
|
||||
|
||||
export component Select {
|
||||
in property <[SelectOption]> options: [];
|
||||
in-out property <int> selected-index: -1;
|
||||
in property <string> placeholder: "Select...";
|
||||
in property <bool> enabled: true;
|
||||
|
||||
callback selected(int, string);
|
||||
|
||||
private property <bool> open: false;
|
||||
private property <bool> hovered: false;
|
||||
|
||||
min-width: 120px;
|
||||
min-height: 36px;
|
||||
|
||||
states [
|
||||
disabled when !root.enabled: {
|
||||
trigger.opacity: 0.5;
|
||||
}
|
||||
]
|
||||
|
||||
VerticalLayout {
|
||||
// Trigger button
|
||||
trigger := Rectangle {
|
||||
height: 36px;
|
||||
background: Theme.colors.background;
|
||||
border-radius: SpacingSystem.radius.md;
|
||||
border-width: 1px;
|
||||
border-color: Theme.colors.input;
|
||||
|
||||
drop-shadow-blur: 1px;
|
||||
drop-shadow-color: #00000010;
|
||||
drop-shadow-offset-y: 1px;
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: SpacingSystem.spacing.s3;
|
||||
padding-right: SpacingSystem.spacing.s3;
|
||||
spacing: SpacingSystem.spacing.s2;
|
||||
alignment: space-between;
|
||||
|
||||
value-text := Text {
|
||||
text: root.selected-index >= 0 && root.selected-index < root.options.length
|
||||
? root.options[root.selected-index].label
|
||||
: root.placeholder;
|
||||
color: root.selected-index >= 0
|
||||
? Theme.colors.foreground
|
||||
: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
chevron := Text {
|
||||
text: "›";
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.base;
|
||||
font-weight: Typography.weights.bold;
|
||||
vertical-alignment: center;
|
||||
width: 16px;
|
||||
horizontal-alignment: center;
|
||||
|
||||
rotation-angle: root.open ? -90deg : 90deg;
|
||||
rotation-origin-x: self.width / 2;
|
||||
rotation-origin-y: self.height / 2;
|
||||
|
||||
animate rotation-angle { duration: Animations.durations.fast; easing: Animations.ease-out; }
|
||||
}
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
enabled: root.enabled;
|
||||
|
||||
clicked => {
|
||||
root.open = !root.open;
|
||||
}
|
||||
|
||||
moved => {
|
||||
root.hovered = self.has-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown menu
|
||||
if root.open: dropdown := Rectangle {
|
||||
y: trigger.height + 4px;
|
||||
background: Theme.colors.background;
|
||||
border-radius: SpacingSystem.radius.md;
|
||||
border-width: 1px;
|
||||
border-color: Theme.colors.border;
|
||||
|
||||
drop-shadow-blur: 8px;
|
||||
drop-shadow-color: #00000020;
|
||||
drop-shadow-offset-y: 4px;
|
||||
|
||||
VerticalLayout {
|
||||
padding: SpacingSystem.spacing.s1;
|
||||
|
||||
for option[index] in root.options: SelectItem {
|
||||
label: option.label;
|
||||
selected: index == root.selected-index;
|
||||
|
||||
clicked => {
|
||||
root.selected-index = index;
|
||||
root.selected(index, option.value);
|
||||
root.open = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component SelectItem {
|
||||
in property <string> label: "";
|
||||
in property <bool> selected: false;
|
||||
|
||||
callback clicked();
|
||||
|
||||
private property <bool> hovered: false;
|
||||
|
||||
height: 32px;
|
||||
|
||||
states [
|
||||
selected when root.selected: {
|
||||
container.background: Theme.colors.accent;
|
||||
}
|
||||
hovered when root.hovered && !root.selected: {
|
||||
container.background: Theme.colors.accent.transparentize(0.5);
|
||||
}
|
||||
]
|
||||
|
||||
container := Rectanglen background: transparent;
|
||||
border-radius: SpacingSystem.radius.sm;
|
||||
|
||||
animate background { duration: Animations.durations.fast; }
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: SpacingSystem.spacing.s2;
|
||||
padding-right: SpacingSystem.spacing.s2;
|
||||
spacing: SpacingSystem.spacing.s2;
|
||||
|
||||
if root.selected: checkmark := Text {
|
||||
text: "✓";
|
||||
color: Theme.colors.primary;
|
||||
font-size: Typography.sizes.sm;
|
||||
font-weight: Typography.weights.bold;
|
||||
vertical-alignment: center;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.label;
|
||||
color: Theme.colors.foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
clicked => {
|
||||
root.clicked();
|
||||
}
|
||||
|
||||
moved => {
|
||||
root.hovered = self.has-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
ui/components/separator.slint
Normal file
20
ui/components/separator.slint
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Theme } from "../theme/theme.slint";
|
||||
|
||||
export enum Orientation {
|
||||
horizontal,
|
||||
vertical,
|
||||
}
|
||||
|
||||
export component Separator {
|
||||
in property <Orientation> orientation: Orientation.horizontal;
|
||||
|
||||
if orientation == Orientation.horizontal: Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.colors.border;
|
||||
}
|
||||
|
||||
if orientation == Orientation.vertical: Rectangle {
|
||||
width: 1px;
|
||||
background: Theme.colors.border;
|
||||
}
|
||||
}
|
||||
57
ui/components/status-indicator.slint
Normal file
57
ui/components/status-indicator.slint
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Theme, Animations } from "../theme/theme.slint";
|
||||
|
||||
export enum StatusState {
|
||||
stopped,
|
||||
starting,
|
||||
running,
|
||||
}
|
||||
|
||||
export component StatusIndicator {
|
||||
in property <StatusState> state: StatusState.stopped;
|
||||
|
||||
private property <color> dot-color: state == StatusState.running ? #22c55e :
|
||||
state == StatusState.starting ? #eab308 :
|
||||
#9ca3af;
|
||||
|
||||
private property <bool> should-animate: state == StatusState.running || state == StatusState.starting;
|
||||
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
|
||||
// Breathing animation ring
|
||||
if root.should-animate: ping := Rectangle {
|
||||
width: parent.width;
|
||||
height: parent.height;
|
||||
border-radius: 5px;
|
||||
background: root.dot-color.transparentize(0.25);
|
||||
|
||||
// Animate scale and opacity for breathing effect
|
||||
states [
|
||||
pulse: {
|
||||
ping.width: parent.width * 2;
|
||||
ping.height: parent.height * 2;
|
||||
ping.x: -parent.width / 2;
|
||||
ping.y: -parent.height / 2;
|
||||
ping.opacity: 0;
|
||||
|
||||
in {
|
||||
animate width, height, x, y, opacity {
|
||||
duration: 1500ms;
|
||||
easing: cubic-bezier(0, 0, 0.2, 1);
|
||||
iteration-count: -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Main dot
|
||||
dot := Rectangle {
|
||||
width: parent.width;
|
||||
height: parent.height;
|
||||
border-radius: 5px;
|
||||
background: root.dot-color;
|
||||
|
||||
animate background { duration: Animations.durations.normal; }
|
||||
}
|
||||
}
|
||||
77
ui/components/switch.slint
Normal file
77
ui/components/switch.slint
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Theme, SpacingSystem, Animations } from "../theme/theme.slint";
|
||||
|
||||
export component Switch {
|
||||
in property <bool> checked: false;
|
||||
in property <bool> enabled: true;
|
||||
|
||||
callback toggled(bool);
|
||||
|
||||
private property <bool> pressed: false;
|
||||
private property <bool> hovered: false;
|
||||
|
||||
min-width: 32px;
|
||||
min-height: 18px;
|
||||
|
||||
states [
|
||||
disabled when !root.enabled: {
|
||||
track.opacity: 0.5;
|
||||
thumb.opacity: 0.5;
|
||||
}
|
||||
checked when root.checked: {
|
||||
track.background: Theme.colors.primary;
|
||||
thumb.x: root.width - thumb.width - 2px;
|
||||
}
|
||||
unchecked when !root.checked: {
|
||||
track.background: Theme.colors.input;
|
||||
thumb.x: 2px;
|
||||
}
|
||||
]
|
||||
|
||||
track := Rectangle {
|
||||
width: 32px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
background: Theme.colors.input;
|
||||
border-width: 1px;
|
||||
border-color: transparent;
|
||||
|
||||
animate background { duration: Animations.durations.fast; easing: Animations.ease-out; }
|
||||
|
||||
thumb := Rectangle {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
x: 2px;
|
||||
border-radius: 7px;
|
||||
background: Theme.colors.background;
|
||||
|
||||
drop-shadow-blur: 2px;
|
||||
drop-shadow-color: #00000040;
|
||||
drop-shadow-offset-y: 1px;
|
||||
|
||||
animate x { duration: Animations.durations.fast; easing: Animations.ease-out; }
|
||||
}
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
enabled: root.enabled;
|
||||
|
||||
clicked => {
|
||||
if (root.enabled) {
|
||||
root.toggled(!root.checked);
|
||||
}
|
||||
}
|
||||
|
||||
pointer-event(event) => {
|
||||
if (event.kind == PointerEventKind.down) {
|
||||
root.pressed = true;
|
||||
} else if (event.kind == PointerEventKind.up || event.kind == PointerEventKind.cancel) {
|
||||
root.pressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
moved => {
|
||||
root.hovered = self.has-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
125
ui/components/tabs.slint
Normal file
125
ui/components/tabs.slint
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Theme, SpacingSystem, Typography, Animations } from "../theme/theme.slint";
|
||||
|
||||
export struct TabItem {
|
||||
title: string,
|
||||
}
|
||||
|
||||
export component Tabs {
|
||||
in property <[TabItem]> tabs: [];
|
||||
in-out property <int> current-index: 0;
|
||||
|
||||
callback tab-changed(int);
|
||||
|
||||
min-height: 200px;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: SpacingSystem.spacing.s2;
|
||||
|
||||
// Tab List
|
||||
tab-list := HorizontalLayout {
|
||||
height: 36px;
|
||||
spacing: 0;
|
||||
|
||||
Rectangle {
|
||||
background: Theme.colors.muted;
|
||||
border-radius: SpacingSystem.radius.lg;
|
||||
padding: 3px;
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: 0;
|
||||
padding: 0;
|
||||
|
||||
for tab[index] in root.tabs: TabTrigger {
|
||||
text: tab.title;
|
||||
active: index == root.current-index;
|
||||
clicked => {
|
||||
root.current-index = index;
|
||||
root.tab-changed(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tab Content Area
|
||||
content-area := Rectangle {
|
||||
// Content will be provided by @children in parent component
|
||||
@children
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component TabTrigger {
|
||||
in property <string> text: "";
|
||||
in property <bool> active: false;
|
||||
|
||||
callback clicked();
|
||||
|
||||
private property <bool> hovered: false;
|
||||
|
||||
min-width: 60px;
|
||||
height: 30px;
|
||||
|
||||
states [
|
||||
active when root.active: {
|
||||
container.background: Theme.colors.background;
|
||||
label.color: Theme.colors.foreground;
|
||||
container.border-color: Theme.colors.input;
|
||||
}
|
||||
inactive when !root.active: {
|
||||
container.background: transparent;
|
||||
label.color: Theme.colors.muted-foreground;
|
||||
container.border-color: transparent;
|
||||
}
|
||||
hovered when root.hovered && !root.active: {
|
||||
label.color: Theme.colors.foreground;
|
||||
}
|
||||
]
|
||||
|
||||
container := Rectangle {
|
||||
border-radius: SpacingSystem.radius.md;
|
||||
background: transparent;
|
||||
border-width: 1px;
|
||||
border-color: transparent;
|
||||
|
||||
drop-shadow-blur: root.active ? 2px : 0;
|
||||
drop-shadow-color: root.active ? #00000010 : transparent;
|
||||
drop-shadow-offset-y: root.active ? 1px : 0;
|
||||
|
||||
animate background { duration: Animations.durations.fast; }
|
||||
animate border-color { duration: Animations.durations.fast; }
|
||||
|
||||
label := Text {
|
||||
text: root.text;
|
||||
color: Theme.colors.muted-foreground;
|
||||
font-size: Typography.sizes.sm;
|
||||
font-weight: Typography.weights.medium;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
|
||||
animate color { duration: Animations.durations.fast; }
|
||||
}
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
clicked => {
|
||||
root.clicked();
|
||||
}
|
||||
|
||||
moved => {
|
||||
root.hovered = self.has-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TabContent component for wrapping content
|
||||
export component TabContent {
|
||||
in property <int> index: 0;
|
||||
in property <int> current-index: 0;
|
||||
|
||||
visible: root.index == root.current-index;
|
||||
|
||||
VerticalLayout {
|
||||
@children
|
||||
}
|
||||
}
|
||||
42
ui/components/tooltip.slint
Normal file
42
ui/components/tooltip.slint
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Theme, SpacingSystem, Typography, Animations } from "../theme/theme.slint";
|
||||
|
||||
export component Tooltip {
|
||||
in property <string> text: "";
|
||||
in property <bool> show: false;
|
||||
|
||||
if root.show: popup := Rectangle {
|
||||
y: -self.height - 8px;
|
||||
x: (parent.width - self.width) / 2;
|
||||
width: tooltip-text.width + SpacingSystem.spacing.s3 * 2;
|
||||
height: tooltip-text.height + SpacingSystem.spacing.s2 * 2;
|
||||
background: Theme.colors.foreground;
|
||||
border-radius: SpacingSystem.radius.sm;
|
||||
|
||||
drop-shadow-blur: 8px;
|
||||
drop-shadow-color: #00000030;
|
||||
drop-shadow-offset-y: 2px;
|
||||
|
||||
opacity: root.show ? 1 : 0;
|
||||
animate opacity { duration: Animations.durations.fast; }
|
||||
|
||||
tooltip-text := Text {
|
||||
text: root.text;
|
||||
color: Theme.colors.background;
|
||||
font-size: Typography.sizes.xs;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
x: SpacingSystem.spacing.s3;
|
||||
y: SpacingSystem.spacing.s2;
|
||||
}
|
||||
|
||||
// Arrow
|
||||
arrow := Path {
|
||||
y: parent.height;
|
||||
x: (parent.width - 8px) / 2;
|
||||
width: 8px;
|
||||
height: 4px;
|
||||
fill: Theme.colors.foreground;
|
||||
commands: "M 0 0 L 4 4 L 8 0 Z";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user