This commit is contained in:
2026-01-26 09:05:48 +08:00
commit f218a21377
20 changed files with 7774 additions and 0 deletions

47
ui/components/badge.slint Normal file
View File

@@ -0,0 +1,47 @@
// Badge Component
// Small status indicator or label
import { Theme, Typography, SpacingSystem } from "../theme/theme.slint";
export component Badge {
// Public properties
in property <string> text: "Badge";
in property <string> variant: "default"; // default | secondary | destructive | outline
// Calculate colors based on variant
private property <color> bg-color: {
if variant == "destructive" { Theme.colors.destructive }
else if variant == "secondary" { Theme.colors.secondary }
else if variant == "outline" { Colors.transparent }
else { Theme.colors.primary }
};
private property <color> text-color: {
if variant == "destructive" { Theme.colors.destructive-foreground }
else if variant == "secondary" { Theme.colors.secondary-foreground }
else if variant == "outline" { Theme.colors.foreground }
else { Theme.colors.primary-foreground }
};
// Fixed height for badge
height: 24px;
Rectangle {
background: bg-color;
border-radius: 12px; // Fixed radius for consistent rounded look
border-width: variant == "outline" ? 1px : 0px;
border-color: Theme.colors.border;
// Center the text
Text {
text: root.text;
font-size: Typography.sizes.xs;
font-weight: Typography.weights.medium;
color: text-color;
vertical-alignment: center;
horizontal-alignment: center;
x: 10px;
width: parent.width - 20px;
}
}
}

102
ui/components/button.slint Normal file
View File

@@ -0,0 +1,102 @@
// Button Component
// Shadcn-style button with multiple variants and sizes
import { Theme, Typography, SpacingSystem } from "../theme/theme.slint";
import { Animations } from "../utils/animations.slint";
export component Button {
// Public properties
in property <string> text: "Button";
in property <string> variant: "default"; // default | destructive | outline | secondary | ghost
in property <string> size: "md"; // sm | md | lg
in property <bool> disabled: false;
// Callbacks
callback clicked();
// Internal state
private property <bool> hovered: false;
// Calculate background color based on variant
private property <color> base-bg-color: {
if disabled { Theme.colors.muted }
else if variant == "destructive" { Theme.colors.destructive }
else if variant == "outline" { Colors.transparent }
else if variant == "secondary" { Theme.colors.secondary }
else if variant == "ghost" { Colors.transparent }
else { Theme.colors.primary }
};
// Calculate hover background color
private property <color> hover-bg-color: {
if disabled { Theme.colors.muted }
else if variant == "destructive" { #dc2626 }
else if variant == "outline" { Theme.colors.accent }
else if variant == "secondary" { #e4e4e7 }
else if variant == "ghost" { Theme.colors.accent }
else { #27272a }
};
// Calculate text color based on variant
private property <color> text-color: {
if disabled { Theme.colors.muted-foreground }
else if variant == "destructive" { Theme.colors.destructive-foreground }
else if variant == "outline" { Theme.colors.foreground }
else if variant == "secondary" { Theme.colors.secondary-foreground }
else if variant == "ghost" { Theme.colors.foreground }
else { Theme.colors.primary-foreground }
};
// Calculate size-based dimensions
private property <length> btn-height: size == "sm" ? 36px : size == "lg" ? 44px : 40px;
private property <length> btn-padding-x: size == "sm" ? 12px : size == "lg" ? 24px : 16px;
private property <length> font-size: size == "sm" ? Typography.sizes.sm : size == "lg" ? Typography.sizes.lg : Typography.sizes.base;
min-width: btn-height;
min-height: btn-height;
// Main container
container := Rectangle {
background: hovered ? hover-bg-color : base-bg-color;
border-radius: SpacingSystem.radius.md;
border-width: variant == "outline" ? 1px : 0px;
border-color: Theme.colors.border;
// Smooth transitions
animate background {
duration: Animations.durations.fast;
easing: Animations.ease-in-out;
}
// Content layout
HorizontalLayout {
padding-left: btn-padding-x;
padding-right: btn-padding-x;
spacing: SpacingSystem.spacing.s2;
alignment: center;
// Button text
Text {
text: root.text;
color: text-color;
font-size: font-size;
font-weight: Typography.weights.medium;
vertical-alignment: center;
horizontal-alignment: center;
}
}
// Touch interaction area
TouchArea {
enabled: !disabled;
clicked => {
root.clicked();
}
moved => {
hovered = self.has-hover;
}
}
}
}

93
ui/components/card.slint Normal file
View File

@@ -0,0 +1,93 @@
// Card Component
// Container for grouping related content with optional hover effect
import { Theme, Typography, SpacingSystem } from "../theme/theme.slint";
import { Animations } from "../utils/animations.slint";
// Main Card component
export component Card {
in property <bool> hoverable: false;
private property <bool> hovered: false;
// Main container
Rectangle {
background: Theme.colors.card;
border-radius: SpacingSystem.radius.lg;
border-width: 1px;
border-color: Theme.colors.border;
// Hover shadow effect
drop-shadow-blur: hovered && hoverable ? 8px : 0px;
drop-shadow-color: #00000020;
drop-shadow-offset-y: hovered && hoverable ? 4px : 0px;
animate drop-shadow-blur, drop-shadow-offset-y {
duration: Animations.durations.normal;
easing: Animations.ease-in-out;
}
VerticalLayout {
padding: SpacingSystem.spacing.s6;
spacing: SpacingSystem.spacing.s4;
@children
}
if hoverable: TouchArea {
moved => {
hovered = self.has-hover;
}
}
}
}
// Card Header component
export component CardHeader {
VerticalLayout {
spacing: SpacingSystem.spacing.s2;
@children
}
}
// Card Title component
export component CardTitle {
in property <string> text;
Text {
text: root.text;
font-size: Typography.sizes.xl;
font-weight: Typography.weights.semibold;
color: Theme.colors.card-foreground;
wrap: word-wrap;
}
}
// Card Description component
export component CardDescription {
in property <string> text;
Text {
text: root.text;
font-size: Typography.sizes.sm;
color: Theme.colors.muted-foreground;
wrap: word-wrap;
}
}
// Card Content component
export component CardContent {
VerticalLayout {
spacing: SpacingSystem.spacing.s4;
@children
}
}
// Card Footer component
export component CardFooter {
HorizontalLayout {
spacing: SpacingSystem.spacing.s2;
alignment: end;
@children
}
}

116
ui/components/dialog.slint Normal file
View File

@@ -0,0 +1,116 @@
// Dialog Component - Simplified version
// Modal dialog with backdrop and smooth animations
import { Theme, Typography, SpacingSystem } from "../theme/theme.slint";
import { Animations } from "../utils/animations.slint";
export component Dialog {
// Public properties
in-out property <bool> open: false;
in property <string> title: "";
in property <string> description: "";
// Callbacks
callback close();
width: 100%;
height: 100%;
// Only show when open
if open: Rectangle {
width: 100%;
height: 100%;
background: #00000080;
// Close on backdrop click
TouchArea {
clicked => {
root.close();
}
}
// Dialog content container (centered)
VerticalLayout {
alignment: center;
HorizontalLayout {
alignment: center;
// Dialog card
Rectangle {
width: 500px;
background: Theme.colors.card;
border-radius: SpacingSystem.radius.lg;
border-width: 1px;
border-color: Theme.colors.border;
drop-shadow-blur: 20px;
drop-shadow-color: #00000040;
VerticalLayout {
padding: SpacingSystem.spacing.s6;
spacing: SpacingSystem.spacing.s4;
// Header
HorizontalLayout {
spacing: SpacingSystem.spacing.s4;
alignment: space-between;
VerticalLayout {
spacing: SpacingSystem.spacing.s2;
// Title
Text {
text: title;
font-size: Typography.sizes.xl;
font-weight: Typography.weights.semibold;
color: Theme.colors.card-foreground;
wrap: word-wrap;
}
// Description
Text {
text: description;
font-size: Typography.sizes.sm;
color: Theme.colors.muted-foreground;
wrap: word-wrap;
}
}
// Close button
Rectangle {
width: 32px;
height: 32px;
border-radius: SpacingSystem.radius.md;
background: close-touch.has-hover ? Theme.colors.muted : Colors.transparent;
animate background {
duration: Animations.durations.fast;
easing: Animations.ease-in-out;
}
Text {
text: "✕";
font-size: Typography.sizes.lg;
color: Theme.colors.muted-foreground;
horizontal-alignment: center;
vertical-alignment: center;
}
close-touch := TouchArea {
clicked => {
root.close();
}
}
}
}
}
// Prevent backdrop click from closing
TouchArea {
// Consume clicks to prevent backdrop from receiving them
}
}
}
}
}
}

74
ui/components/input.slint Normal file
View File

@@ -0,0 +1,74 @@
// Input Component
// Text input field with focus states and optional icons
import { Theme, Typography, SpacingSystem } from "../theme/theme.slint";
import { Animations } from "../utils/animations.slint";
export component Input {
// Public properties
in-out property <string> value: "";
in property <string> placeholder: "";
in property <bool> disabled: false;
in property <bool> error: false;
// Callbacks
callback edited(string);
callback accepted(string);
// Internal state
private property <bool> focused: false;
min-height: 40px;
// Main container
Rectangle {
background: Theme.colors.background;
border-radius: SpacingSystem.radius.md;
border-width: 1px;
border-color: error ? Theme.colors.destructive :
focused ? Theme.colors.ring :
Theme.colors.input;
// Smooth border color transition
animate border-color {
duration: Animations.durations.fast;
easing: Animations.ease-in-out;
}
HorizontalLayout {
padding: SpacingSystem.spacing.s2;
spacing: SpacingSystem.spacing.s2;
// Text input field
input-field := TextInput {
text <=> value;
enabled: !disabled;
font-size: Typography.sizes.base;
color: Theme.colors.foreground;
selection-background-color: Theme.colors.primary;
selection-foreground-color: Theme.colors.primary-foreground;
edited => {
root.edited(self.text);
}
accepted => {
root.accepted(self.text);
}
}
}
// Focus detection
FocusScope {
key-pressed(event) => {
focused = input-field.has-focus;
return accept;
}
key-released(event) => {
focused = input-field.has-focus;
return accept;
}
}
}
}

89
ui/components/item.slint Normal file
View File

@@ -0,0 +1,89 @@
// Item Component
// List item with icon, title, description and selection state
import { Theme, Typography, SpacingSystem } from "../theme/theme.slint";
import { Animations } from "../utils/animations.slint";
export component Item {
// Public properties
in property <string> icon: ""; // Text icon/emoji
in property <string> title;
in property <string> description: "";
in property <bool> selected: false;
// Callbacks
callback clicked();
// Internal state
private property <bool> hovered: false;
min-height: 60px;
// Main container
Rectangle {
background: selected ? Theme.colors.accent :
hovered ? Theme.colors.muted :
Colors.transparent;
border-radius: SpacingSystem.radius.md;
animate background {
duration: Animations.durations.fast;
easing: Animations.ease-in-out;
}
HorizontalLayout {
padding: SpacingSystem.spacing.s3;
spacing: SpacingSystem.spacing.s3;
alignment: start;
// Icon (if provided)
if icon != "": Rectangle {
width: 40px;
height: 40px;
border-radius: SpacingSystem.radius.md;
background: Theme.colors.muted;
Text {
text: icon;
font-size: Typography.sizes.xl;
horizontal-alignment: center;
vertical-alignment: center;
}
}
// Content area
VerticalLayout {
spacing: SpacingSystem.spacing.s1;
alignment: center;
// Title
Text {
text: title;
font-size: Typography.sizes.base;
font-weight: Typography.weights.medium;
color: Theme.colors.foreground;
wrap: word-wrap;
}
// Description (if provided)
if description != "": Text {
text: description;
font-size: Typography.sizes.sm;
color: Theme.colors.muted-foreground;
wrap: word-wrap;
}
}
}
// Touch interaction
TouchArea {
clicked => {
root.clicked();
}
moved => {
hovered = self.has-hover;
}
}
}
}

138
ui/components/sidebar.slint Normal file
View File

@@ -0,0 +1,138 @@
// Sidebar Component
// Collapsible navigation sidebar with menu items
import { Theme, Typography, SpacingSystem } from "../theme/theme.slint";
import { Animations } from "../utils/animations.slint";
// Navigation item structure
export struct NavItem {
icon: string, // Text icon/emoji
label: string,
active: bool,
}
export component Sidebar {
// Public properties
in-out property <bool> collapsed: false;
in property <[NavItem]> items: [];
// Callbacks
callback item-clicked(int); // Pass item index
callback toggle-collapsed();
// Calculate width based on collapsed state
private property <length> sidebar-width: collapsed ? 60px : 240px;
width: sidebar-width;
animate width {
duration: Animations.durations.normal;
easing: Animations.ease-in-out;
}
// Main container
Rectangle {
background: Theme.colors.card;
Rectangle {
x: parent.width - 1px;
width: 1px;
height: parent.height;
background: Theme.colors.border;
}
VerticalLayout {
padding: SpacingSystem.spacing.s4;
spacing: SpacingSystem.spacing.s2;
// Toggle button
Rectangle {
height: 40px;
background: Colors.transparent;
border-radius: SpacingSystem.radius.md;
HorizontalLayout {
padding: SpacingSystem.spacing.s2;
spacing: SpacingSystem.spacing.s2;
Text {
text: collapsed ? "☰" : "✕";
font-size: Typography.sizes.xl;
color: Theme.colors.foreground;
horizontal-alignment: center;
vertical-alignment: center;
}
if !collapsed: Text {
text: "Menu";
font-size: Typography.sizes.base;
font-weight: Typography.weights.medium;
color: Theme.colors.foreground;
vertical-alignment: center;
}
}
TouchArea {
clicked => {
root.toggle-collapsed();
}
}
}
// Divider
Rectangle {
height: 1px;
background: Theme.colors.border;
}
// Navigation items
for item[index] in items: Rectangle {
height: 44px;
background: item.active ? Theme.colors.accent : Colors.transparent;
border-radius: SpacingSystem.radius.md;
states [
hovered when touch-area.has-hover && !item.active: {
background: Theme.colors.muted;
}
]
animate background {
duration: Animations.durations.fast;
easing: Animations.ease-in-out;
}
HorizontalLayout {
padding: SpacingSystem.spacing.s2;
spacing: SpacingSystem.spacing.s3;
alignment: start;
// Icon
Text {
text: item.icon;
font-size: Typography.sizes.xl;
color: item.active ? Theme.colors.accent-foreground : Theme.colors.foreground;
horizontal-alignment: center;
vertical-alignment: center;
width: 24px;
}
// Label (only when not collapsed)
if !collapsed: Text {
text: item.label;
font-size: Typography.sizes.base;
font-weight: item.active ? Typography.weights.medium : Typography.weights.normal;
color: item.active ? Theme.colors.accent-foreground : Theme.colors.foreground;
vertical-alignment: center;
}
}
touch-area := TouchArea {
clicked => {
root.item-clicked(index);
}
}
}
}
}
}

148
ui/components/toast.slint Normal file
View File

@@ -0,0 +1,148 @@
// Toast Component
// Notification toast with auto-dismiss and animations
import { Theme, Typography, SpacingSystem } from "../theme/theme.slint";
import { Animations } from "../utils/animations.slint";
// Toast message structure
export struct ToastMessage {
message: string,
variant: string, // default | success | error | warning
show: bool,
}
// Single toast item
export component Toast {
// Public properties
in property <string> message: "";
in property <string> variant: "default"; // default | success | error | warning
in-out property <bool> show: false;
// Callbacks
callback dismissed();
// Calculate colors based on variant
private property <color> bg-color: {
if variant == "success" { #10b981 }
else if variant == "error" { Theme.colors.destructive }
else if variant == "warning" { #f59e0b }
else { Theme.colors.card }
};
private property <color> text-color: {
if variant == "success" { #ffffff }
else if variant == "error" { Theme.colors.destructive-foreground }
else if variant == "warning" { #ffffff }
else { Theme.colors.card-foreground }
};
private property <string> icon: {
if variant == "success" { "✓" }
else if variant == "error" { "✕" }
else if variant == "warning" { "⚠" }
else { "" }
};
// Animation properties
property <length> translate-y: show ? 0px : -20px;
property <float> opacity-val: show ? 1.0 : 0.0;
animate translate-y, opacity-val {
duration: Animations.durations.normal;
easing: Animations.ease-out;
}
width: 350px;
height: show ? 60px : 0px;
if show: Rectangle {
y: translate-y;
opacity: opacity-val;
background: bg-color;
border-radius: SpacingSystem.radius.lg;
border-width: 1px;
border-color: Theme.colors.border;
drop-shadow-blur: 10px;
drop-shadow-color: #00000020;
drop-shadow-offset-y: 4px;
HorizontalLayout {
padding: SpacingSystem.spacing.s4;
spacing: SpacingSystem.spacing.s3;
alignment: space-between;
HorizontalLayout {
spacing: SpacingSystem.spacing.s3;
// Icon
Text {
text: icon;
font-size: Typography.sizes.xl;
color: text-color;
vertical-alignment: center;
}
// Message
Text {
text: message;
font-size: Typography.sizes.base;
color: text-color;
vertical-alignment: center;
wrap: word-wrap;
}
}
// Close button
Rectangle {
width: 24px;
height: 24px;
border-radius: SpacingSystem.radius.sm;
background: close-touch.has-hover ? #00000020 : Colors.transparent;
animate background {
duration: Animations.durations.fast;
easing: Animations.ease-in-out;
}
Text {
text: "✕";
font-size: Typography.sizes.sm;
color: text-color;
horizontal-alignment: center;
vertical-alignment: center;
}
close-touch := TouchArea {
clicked => {
root.dismissed();
}
}
}
}
}
}
// Toast container (manages multiple toasts)
export component ToastContainer {
in property <[ToastMessage]> toasts: [];
callback toast-dismissed(int);
// Position at top-right corner
width: 370px;
VerticalLayout {
spacing: SpacingSystem.spacing.s2;
alignment: start;
for toast[index] in toasts: Toast {
message: toast.message;
variant: toast.variant;
show: toast.show;
dismissed => {
root.toast-dismissed(index);
}
}
}
}