feat(qml): MD3 rework batch 2 — 17 more components rewritten

Feedback: CAlert (tonal + accent bar), CDialog (radius 28, scale anim), CSnackbar (inverse surface, slide-in)
Navigation: CTabBar (animated indicator pill), CListItem (state layers), CBreadcrumbs (full rewrite)
Data: CAvatar (tonal primary), CDivider (theme-aware), CTable (hover rows, sort arrows, proper padding)
Typography: CText (full MD3 type scale inline), CTitle (extends CText), CCodeBlock (radius 12), CCodeInline (radius 4)
Forms: CFormGroup (focus/error states), CFormLabel (animated color), CLabel (control association), CAutocomplete (styled popup)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 04:03:58 +00:00
parent eecaac8634
commit de3a3ac194
17 changed files with 1092 additions and 532 deletions

View File

@@ -4,56 +4,56 @@ import QtQuick.Layouts
import QmlComponents 1.0
/**
* CCodeBlock.qml - Code block display (mirrors _code-block.scss)
* Syntax-highlighted code with optional copy button
* CCodeBlock.qml - Material Design 3 code block
* Displays code with optional line numbers, copy button, and horizontal scroll
*/
Rectangle {
id: root
property string code: ""
property string language: ""
property bool showCopy: true
property bool showLineNumbers: false
property int maxHeight: 400
color: Theme.mode === "dark" ? Qt.rgba(0, 0, 0, 0.4) : Qt.rgba(0, 0, 0, 0.05)
radius: StyleVariables.radiusSm
color: Qt.lighter(Theme.surface, 1.15)
radius: 12
implicitWidth: parent ? parent.width : 400
implicitHeight: Math.min(contentCol.implicitHeight + StyleVariables.spacingMd * 2, maxHeight)
implicitHeight: Math.min(contentCol.implicitHeight + 32, maxHeight)
ColumnLayout {
id: contentCol
anchors.fill: parent
anchors.margins: StyleVariables.spacingMd
spacing: StyleVariables.spacingSm
// Header with language and copy button
anchors.margins: 16
spacing: 8
// Header with language label and copy button
RowLayout {
Layout.fillWidth: true
visible: root.language !== "" || root.showCopy
Text {
text: root.language
color: Theme.onSurfaceVariant
font.pixelSize: StyleVariables.fontSizeXs
font.family: StyleVariables.fontMono
color: Theme.textSecondary
font.pixelSize: 12
font.family: Theme.fontFamilyMono
font.weight: Font.Medium
visible: root.language !== ""
}
Item { Layout.fillWidth: true }
CButton {
text: "Copy"
size: "sm"
variant: "text"
visible: root.showCopy
onClicked: {
// Copy to clipboard would need Python bridge
text = "Copied!"
copyTimer.start()
}
Timer {
id: copyTimer
interval: 2000
@@ -61,49 +61,50 @@ Rectangle {
}
}
}
// Code content
// Scrollable code content
Flickable {
Layout.fillWidth: true
Layout.fillHeight: true
contentWidth: codeText.implicitWidth
contentHeight: codeText.implicitHeight
contentWidth: codeRow.implicitWidth
contentHeight: codeRow.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AsNeeded }
RowLayout {
spacing: StyleVariables.spacingSm
// Line numbers
id: codeRow
spacing: 12
// Line numbers column
Column {
visible: root.showLineNumbers
spacing: 0
Repeater {
model: root.code.split('\n').length
Text {
text: (index + 1).toString()
color: Theme.onSurfaceVariant
font.family: StyleVariables.fontMono
font.pixelSize: StyleVariables.fontSizeSm
color: Theme.textSecondary
font.family: Theme.fontFamilyMono
font.pixelSize: 14
horizontalAlignment: Text.AlignRight
width: 30
opacity: 0.5
width: 32
opacity: 0.6
}
}
}
// Code text
Text {
id: codeText
text: root.code
color: Theme.onSurface
font.family: StyleVariables.fontMono
font.pixelSize: StyleVariables.fontSizeSm
color: Theme.text
font.family: Theme.fontFamilyMono
font.pixelSize: 14
textFormat: Text.PlainText
wrapMode: Text.NoWrap
}

View File

@@ -2,26 +2,26 @@ import QtQuick
import QmlComponents 1.0
/**
* CCodeInline.qml - Inline code (mirrors _code-inline.scss)
* Small inline code snippets
* CCodeInline.qml - Material Design 3 inline code
* Small inline code snippet with subtle background
*/
Rectangle {
id: root
property alias text: label.text
property alias textColor: label.color
color: Theme.mode === "dark" ? Qt.rgba(255, 255, 255, 0.1) : Qt.rgba(0, 0, 0, 0.06)
radius: StyleVariables.radiusSm
implicitWidth: label.implicitWidth + StyleVariables.spacingSm * 2
implicitHeight: label.implicitHeight + StyleVariables.spacingXs
color: Theme.surface
radius: 4
implicitWidth: label.implicitWidth + 8
implicitHeight: label.implicitHeight + 4
Text {
id: label
anchors.centerIn: parent
color: Theme.onSurface
font.family: StyleVariables.fontMono
font.pixelSize: StyleVariables.fontSizeSm * 0.9
color: Theme.text
font.family: Theme.fontFamilyMono
font.pixelSize: 13
}
}

View File

@@ -2,47 +2,103 @@ import QtQuick
import QmlComponents 1.0
/**
* CText.qml - Styled text component (mirrors SCSS text utilities)
* Pre-configured text with typography variants and color options
*
* CText.qml - Material Design 3 typography component
* Implements the full MD3 type scale with variant and color support
*
* Usage:
* CText { text: "Body text" }
* CText { variant: "h4"; text: "Heading" }
* CText { variant: "caption"; color: "secondary" }
* CText { variant: "caption"; colorVariant: "secondary" }
* CText { variant: "body2"; mono: true }
*/
Text {
id: root
// Public properties
property string variant: "body1" // h1-h6, subtitle1, subtitle2, body1, body2, caption, overline, button
property string colorVariant: "primary" // primary, secondary, disabled, error, success, warning, info
property string variant: "body1" // h1-h6, subtitle1, subtitle2, body1, body2, caption, overline, button
property string colorVariant: "primary" // primary, secondary, disabled, error, success, warning, info, inherit
property bool mono: false
property bool truncate: false
// Get typography settings from mixins
readonly property var _typography: StyleMixins.typography(variant)
// Apply typography
font.pixelSize: _typography.size
font.weight: _typography.weight
font.letterSpacing: _typography.spacing
font.family: mono ? StyleVariables.fontMono : StyleVariables.fontFamily
// Apply color
// MD3 Typography Scale
font.pixelSize: {
switch (variant) {
case "h1": return 57
case "h2": return 45
case "h3": return 36
case "h4": return 32
case "h5": return 28
case "h6": return 24
case "subtitle1": return 16
case "subtitle2": return 14
case "body1": return 16
case "body2": return 14
case "caption": return 12
case "overline": return 12
case "button": return 14
default: return 16
}
}
font.weight: {
switch (variant) {
case "h1": return Font.Normal
case "h2": return Font.Normal
case "h3": return Font.Normal
case "h4": return Font.Normal
case "h5": return Font.Normal
case "h6": return Font.Medium
case "subtitle1": return Font.Medium
case "subtitle2": return Font.Medium
case "body1": return Font.Normal
case "body2": return Font.Normal
case "caption": return Font.Normal
case "overline": return Font.Medium
case "button": return Font.Medium
default: return Font.Normal
}
}
font.letterSpacing: {
switch (variant) {
case "h1": return -0.25
case "h2": return 0
case "h3": return 0
case "h4": return 0
case "h5": return 0
case "h6": return 0
case "subtitle1": return 0.15
case "subtitle2": return 0.1
case "body1": return 0.5
case "body2": return 0.25
case "caption": return 0.4
case "overline": return 1.5
case "button": return 0.1
default: return 0
}
}
font.capitalization: variant === "overline" ? Font.AllUppercase : Font.MixedCase
font.family: mono ? Theme.fontFamilyMono : Theme.fontFamily
lineHeight: variant === "body1" ? 1.5 : 1.0
lineHeightMode: Text.ProportionalHeight
// MD3 color mapping
color: {
switch (colorVariant) {
case "secondary": return Theme.textSecondary
case "disabled": return Theme.textDisabled
case "error": return Theme.error
case "success": return Theme.success
case "warning": return Theme.warning
case "info": return Theme.info
case "inherit": return parent ? parent.color : Theme.text
default: return Theme.text
case "disabled": return Theme.textDisabled
case "error": return Theme.error
case "success": return Theme.success
case "warning": return Theme.warning
case "info": return Theme.info
case "inherit": return parent ? parent.color : Theme.text
default: return Theme.text
}
}
// Truncation
elide: truncate ? Text.ElideRight : Text.ElideNone
maximumLineCount: truncate ? 1 : undefined

View File

@@ -2,51 +2,22 @@ import QtQuick
import QmlComponents 1.0
/**
* CTitle.qml - Title text (mirrors _title.scss)
* Heading typography component
* CTitle.qml - Material Design 3 heading wrapper
* Convenience component around CText for section headings
*
* Usage:
* CTitle { text: "Page Title" }
* CTitle { level: "h3"; text: "Section" }
* CTitle { level: "h5"; text: "Subsection"; gutterBottom: false }
*/
Text {
CText {
id: root
property string level: "h1" // h1, h2, h3, h4, h5, h6
property bool gutterBottom: true // Add bottom margin
color: Theme.onSurface
property string level: "h1" // h1-h6
property bool gutterBottom: true // Add bottom margin
variant: level
wrapMode: Text.Wrap
// Typography based on level
font.pixelSize: {
switch (level) {
case "h1": return 32
case "h2": return 28
case "h3": return 24
case "h4": return 20
case "h5": return 18
case "h6": return 16
default: return 32
}
}
font.weight: {
switch (level) {
case "h1": return Font.Bold
case "h2": return Font.Bold
case "h3": return Font.DemiBold
case "h4": return Font.DemiBold
case "h5": return Font.Medium
case "h6": return Font.Medium
default: return Font.Bold
}
}
lineHeight: {
switch (level) {
case "h1": return 1.2
case "h2": return 1.25
case "h3": return 1.3
default: return 1.4
}
}
bottomPadding: gutterBottom ? StyleVariables.spacingSm : 0
}

View File

@@ -1,10 +1,11 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "../theming"
Rectangle {
id: listItem
property string title: ""
property string subtitle: ""
property string caption: ""
@@ -14,20 +15,30 @@ Rectangle {
property bool selected: false
property bool showDivider: true
property alias trailing: trailingLoader.sourceComponent
signal clicked()
signal trailingClicked()
implicitHeight: Math.max(56, contentColumn.implicitHeight + 16)
color: {
if (selected) return "#1a3a5c"
if (mouseArea.containsMouse) return "#2d2d2d"
return "transparent"
implicitHeight: subtitle || caption ? 56 : 48
radius: 0
color: "transparent"
// MD3 state layer
Rectangle {
anchors.fill: parent
radius: 0
color: {
if (listItem.selected)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
if (mouseArea.containsMouse)
return Qt.rgba(Theme.text.r, Theme.text.g, Theme.text.b, 0.08)
return "transparent"
}
Behavior on color { ColorAnimation { duration: 150 } }
}
Behavior on color { ColorAnimation { duration: 150 } }
MouseArea {
id: mouseArea
anchors.fill: parent
@@ -35,80 +46,87 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onClicked: listItem.clicked()
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 16
spacing: 12
// Leading icon
spacing: 16
// Leading element area (40px)
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
Layout.alignment: Qt.AlignVCenter
radius: 20
color: "#2d2d2d"
visible: listItem.leadingIcon
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
visible: listItem.leadingIcon !== ""
Text {
anchors.centerIn: parent
text: listItem.leadingIcon
font.pixelSize: 18
color: Theme.primary
}
}
// Content
ColumnLayout {
id: contentColumn
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 2
Text {
Layout.fillWidth: true
text: listItem.title
font.pixelSize: 14
font.weight: Font.Medium
color: "#ffffff"
font.weight: Font.Normal
color: listItem.selected ? Theme.primary : Theme.text
elide: Text.ElideRight
}
Text {
Layout.fillWidth: true
text: listItem.subtitle
font.pixelSize: 12
color: "#888888"
color: Theme.textSecondary
elide: Text.ElideRight
visible: listItem.subtitle
visible: listItem.subtitle !== ""
}
Text {
Layout.fillWidth: true
text: listItem.caption
font.pixelSize: 11
color: "#666666"
color: Theme.textMuted
elide: Text.ElideRight
visible: listItem.caption
visible: listItem.caption !== ""
}
}
// Trailing
// Trailing text
Text {
text: listItem.trailingText
font.pixelSize: 12
color: "#888888"
visible: listItem.trailingText
color: Theme.textSecondary
visible: listItem.trailingText !== ""
}
// Trailing custom content
Loader {
id: trailingLoader
}
// Trailing icon
Text {
text: listItem.trailingIcon
font.pixelSize: 16
color: trailingMouseArea.containsMouse ? "#ffffff" : "#888888"
visible: listItem.trailingIcon
color: trailingMouseArea.containsMouse ? Theme.text : Theme.textSecondary
visible: listItem.trailingIcon !== ""
Behavior on color { ColorAnimation { duration: 150 } }
MouseArea {
id: trailingMouseArea
anchors.fill: parent
@@ -122,15 +140,17 @@ Rectangle {
}
}
}
// Divider
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: listItem.leadingIcon ? 68 : 16
anchors.leftMargin: listItem.leadingIcon !== "" ? 72 : 16
anchors.rightMargin: 16
height: 1
color: "#2d2d2d"
color: Theme.border
opacity: 0.5
visible: listItem.showDivider
}
}

View File

@@ -2,41 +2,59 @@ import QtQuick
import QmlComponents 1.0
/**
* CAvatar.qml - Circular avatar (mirrors _avatar.scss)
* Displays image or initials in circular container
* CAvatar.qml - Material Design 3 circular avatar
* Displays image or initials in a circular container with tonal surface
*
* Usage:
* CAvatar { initials: "JD" } // Tonal primary with initials
* CAvatar { src: "avatar.png" } // Image avatar
* CAvatar { size: "lg"; initials: "AB" } // Large avatar
* CAvatar { size: "sm"; bgColor: Theme.error } // Custom color
*/
Rectangle {
id: root
property string size: "md" // sm, md, lg
property string src: "" // Image source URL
property string initials: "" // Fallback initials (e.g. "JD")
property color bgColor: Theme.surfaceVariant
property color textColor: Theme.onSurfaceVariant
// Size mapping
property color bgColor: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16)
property color textColor: Theme.primary
// MD3 size mapping: sm=32, md=40, lg=56
readonly property int _size: {
switch (size) {
case "sm": return 32
case "lg": return 64
default: return 48
case "lg": return 56
default: return 40
}
}
// MD3 font size: scales with avatar size
readonly property int _fontSize: {
switch (size) {
case "sm": return 13
case "lg": return 22
default: return 16
}
}
width: _size
height: _size
radius: _size / 2
color: src ? "transparent" : bgColor
color: src !== "" ? "transparent" : bgColor
clip: true
// Image (if src provided)
// Image avatar
Image {
id: avatarImage
anchors.fill: parent
source: root.src
fillMode: Image.PreserveAspectCrop
visible: root.src !== ""
// Smooth circular clipping
visible: root.src !== "" && status === Image.Ready
smooth: true
asynchronous: true
// Circular clipping via layer
layer.enabled: true
layer.effect: Item {
Rectangle {
@@ -45,14 +63,33 @@ Rectangle {
}
}
}
// Initials fallback
// Placeholder shown while image loads or on error
Rectangle {
anchors.fill: parent
radius: width / 2
color: root.bgColor
visible: root.src !== "" && avatarImage.status !== Image.Ready
Text {
anchors.centerIn: parent
text: root.initials.toUpperCase()
color: root.textColor
font.pixelSize: root._fontSize
font.weight: Font.Medium
font.family: Theme.fontFamily
visible: root.initials !== ""
}
}
// Initials fallback (no src)
Text {
anchors.centerIn: parent
text: root.initials.toUpperCase()
color: root.textColor
font.pixelSize: root._size * 0.4
font.pixelSize: root._fontSize
font.weight: Font.Medium
font.family: Theme.fontFamily
visible: root.src === "" && root.initials !== ""
}
}

View File

@@ -2,27 +2,31 @@ import QtQuick
import QmlComponents 1.0
/**
* CDivider.qml - Divider/separator component (mirrors _divider.scss)
* Horizontal or vertical line separator
*
* CDivider.qml - Material Design 3 divider
* Thin line separator, horizontal or vertical
*
* Usage:
* CDivider {} // Horizontal divider
* CDivider { orientation: "vertical" } // Vertical divider
* CDivider { text: "OR" } // Divider with text
* CDivider {} // Full-width horizontal
* CDivider { variant: "inset" } // 16px left inset
* CDivider { orientation: "vertical" } // Vertical divider
* CDivider { text: "OR" } // Divider with centered text
*/
Item {
id: root
// Public properties
property string orientation: "horizontal" // horizontal, vertical
property string text: ""
property string variant: "fullWidth" // fullWidth, inset, middle
property int inset: StyleVariables.spacingLg
property int inset: 16
// MD3: 1px line using outlineVariant (softer than border)
readonly property color _lineColor: Theme.border
// Size
implicitWidth: orientation === "horizontal" ? 200 : 1
implicitHeight: orientation === "horizontal" ? (text ? 24 : 1) : 200
// Horizontal divider
Row {
visible: root.orientation === "horizontal"
@@ -30,42 +34,43 @@ Item {
anchors.leftMargin: root.variant === "inset" ? root.inset : (root.variant === "middle" ? root.inset : 0)
anchors.rightMargin: root.variant === "middle" ? root.inset : 0
spacing: root.text ? StyleVariables.spacingMd : 0
// Left line
Rectangle {
width: root.text ? (parent.width - textLabel.width - StyleVariables.spacingMd * 2) / 2 : parent.width
height: 1
anchors.verticalCenter: parent.verticalCenter
color: Theme.divider
color: root._lineColor
}
// Text label
Text {
id: textLabel
visible: root.text
visible: root.text !== ""
text: root.text
font.pixelSize: StyleVariables.fontSizeXs
font.weight: Font.Medium
font.family: Theme.fontFamily
color: Theme.textSecondary
anchors.verticalCenter: parent.verticalCenter
}
// Right line
Rectangle {
visible: root.text
visible: root.text !== ""
width: (parent.width - textLabel.width - StyleVariables.spacingMd * 2) / 2
height: 1
anchors.verticalCenter: parent.verticalCenter
color: Theme.divider
color: root._lineColor
}
}
// Vertical divider
Rectangle {
visible: root.orientation === "vertical"
width: 1
height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.divider
color: root._lineColor
}
}

View File

@@ -3,116 +3,196 @@ import QtQuick.Layouts
import QmlComponents 1.0
/**
* CTable.qml - Data table (mirrors _table.scss)
* Simple table with headers and rows
* CTable.qml - Material Design 3 data table
* Surface container header, 48px rows, hover state layer, outlineVariant borders
*
* Usage:
* CTable {
* headers: ["Name", "Email", "Role"]
* rows: [["Alice", "alice@co", "Admin"], ["Bob", "bob@co", "User"]]
* sortColumn: 0; sortAscending: true
* }
*/
Rectangle {
id: root
property var headers: [] // Array of header strings
property var rows: [] // Array of row arrays
property var columnWidths: [] // Optional column width ratios
property bool striped: true
property bool bordered: true
// MD3 sort support
property int sortColumn: -1 // Column index currently sorted (-1 = none)
property bool sortAscending: true
signal headerClicked(int columnIndex)
color: "transparent"
radius: StyleVariables.radiusSm
border.width: bordered ? 1 : 0
border.color: Theme.divider
border.color: Theme.border
implicitWidth: parent ? parent.width : 400
implicitHeight: tableCol.implicitHeight
clip: true
ColumnLayout {
id: tableCol
anchors.fill: parent
spacing: 0
// Header row
// Header row - MD3 surfaceContainer background
Rectangle {
Layout.fillWidth: true
implicitHeight: headerRow.implicitHeight
color: Theme.mode === "dark" ? Qt.rgba(255, 255, 255, 0.08) : Qt.rgba(0, 0, 0, 0.04)
implicitHeight: 48
color: Theme.mode === "dark"
? Qt.rgba(1, 1, 1, 0.08)
: Qt.rgba(0, 0, 0, 0.04)
RowLayout {
id: headerRow
anchors.fill: parent
spacing: 0
Repeater {
model: root.headers
Rectangle {
Item {
Layout.fillWidth: root.columnWidths.length === 0
Layout.preferredWidth: root.columnWidths[index] || -1
implicitHeight: headerText.implicitHeight + StyleVariables.spacingSm * 2
color: "transparent"
border.width: root.bordered && index > 0 ? 1 : 0
border.color: Theme.divider
Text {
id: headerText
Layout.preferredWidth: root.columnWidths.length > index ? root.columnWidths[index] : -1
implicitHeight: 48
// MD3 header cell content
Row {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 16
anchors.rightMargin: 16
spacing: 4
Text {
text: modelData
color: Theme.textSecondary
font.pixelSize: 14
font.weight: Font.DemiBold
font.family: Theme.fontFamily
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
// Sort indicator arrow
Text {
visible: root.sortColumn === index
text: root.sortAscending ? "\u25B2" : "\u25BC"
color: Theme.textSecondary
font.pixelSize: 10
anchors.verticalCenter: parent.verticalCenter
}
}
// Clickable area for sorting
MouseArea {
anchors.fill: parent
anchors.margins: StyleVariables.spacingSm
text: modelData
color: Theme.onSurface
font.pixelSize: StyleVariables.fontSizeSm
font.weight: Font.DemiBold
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
cursorShape: Qt.PointingHandCursor
onClicked: root.headerClicked(index)
}
// Column separator
Rectangle {
visible: root.bordered && index > 0
width: 1
height: parent.height
anchors.left: parent.left
color: Theme.border
}
}
}
}
}
// Bottom border under header
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.border
}
// Data rows
Repeater {
model: root.rows
Rectangle {
id: rowDelegate
Layout.fillWidth: true
implicitHeight: dataRow.implicitHeight
color: root.striped && index % 2 === 1
? (Theme.mode === "dark" ? Qt.rgba(255, 255, 255, 0.02) : Qt.rgba(0, 0, 0, 0.02))
: "transparent"
// Top border
implicitHeight: 48
property bool hovered: rowMouse.containsMouse
// MD3: alternating tint + hover state layer (4%)
color: {
if (hovered)
return Theme.mode === "dark"
? Qt.rgba(1, 1, 1, 0.04)
: Qt.rgba(0, 0, 0, 0.04)
if (root.striped && index % 2 === 1)
return Theme.mode === "dark"
? Qt.rgba(1, 1, 1, 0.02)
: Qt.rgba(0, 0, 0, 0.02)
return "transparent"
}
MouseArea {
id: rowMouse
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
// Row border (outlineVariant between rows)
Rectangle {
width: parent.width
height: root.bordered ? 1 : 0
color: Theme.divider
anchors.bottom: parent.bottom
color: Theme.border
}
RowLayout {
id: dataRow
anchors.fill: parent
anchors.topMargin: root.bordered ? 1 : 0
spacing: 0
Repeater {
model: modelData
Rectangle {
Item {
Layout.fillWidth: root.columnWidths.length === 0
Layout.preferredWidth: root.columnWidths[index] || -1
implicitHeight: cellText.implicitHeight + StyleVariables.spacingSm * 2
color: "transparent"
border.width: root.bordered && index > 0 ? 1 : 0
border.color: Theme.divider
Layout.preferredWidth: root.columnWidths.length > index ? root.columnWidths[index] : -1
implicitHeight: 48
Text {
id: cellText
anchors.fill: parent
anchors.margins: StyleVariables.spacingSm
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 16
anchors.rightMargin: 16
text: modelData
color: Theme.onSurface
font.pixelSize: StyleVariables.fontSizeSm
color: Theme.text
font.pixelSize: 14
font.family: Theme.fontFamily
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
// Column separator
Rectangle {
visible: root.bordered && index > 0
width: 1
height: parent.height
anchors.left: parent.left
color: Theme.border
}
}
}
}

View File

@@ -3,9 +3,9 @@ import QtQuick.Layouts
import QmlComponents 1.0
/**
* CAlert.qml - Alert/notification component (mirrors _alert.scss)
* CAlert.qml - Material Design 3 Alert component
* Displays contextual feedback messages with severity levels
*
*
* Usage:
* CAlert {
* severity: "error"
@@ -15,7 +15,7 @@ import QmlComponents 1.0
*/
Rectangle {
id: root
// Public properties
property string text: ""
property string title: ""
@@ -23,10 +23,10 @@ Rectangle {
property string icon: "" // Custom icon, auto-selected if empty
property bool closable: false
property string variant: "filled" // filled, outlined, standard
// Signals
signal closed()
// Auto-select icon based on severity
readonly property string _effectiveIcon: icon || {
"info": "",
@@ -34,91 +34,128 @@ Rectangle {
"warning": "⚠️",
"error": "❌"
}[severity] || ""
// Get colors based on severity
// MD3 severity color
readonly property color _severityColor: {
switch (severity) {
case "success": return Theme.success
case "error": return Theme.error
case "warning": return Theme.warning
case "info": return Theme.info
default: return Theme.info
}
}
// MD3 tonal background based on severity (12% opacity)
readonly property color _bgColor: {
if (variant === "outlined") return "transparent"
if (variant === "standard") return Qt.rgba(_accentColor.r, _accentColor.g, _accentColor.b, 0.08)
// filled
return Qt.rgba(_accentColor.r, _accentColor.g, _accentColor.b, 0.15)
if (variant === "standard") return Qt.rgba(_severityColor.r, _severityColor.g, _severityColor.b, 0.08)
// filled — MD3 tonal surface
return Qt.rgba(_severityColor.r, _severityColor.g, _severityColor.b, 0.12)
}
readonly property color _accentColor: StyleMixins.statusColor(severity)
readonly property color _accentColor: _severityColor
readonly property color _textColor: {
if (variant === "filled") return _accentColor
if (variant === "filled") return _severityColor
return Theme.text
}
// Size and appearance
implicitHeight: contentLayout.implicitHeight + StyleVariables.spacingMd * 2
implicitHeight: contentLayout.implicitHeight + 16 * 2
implicitWidth: 300
color: _bgColor
radius: StyleVariables.radiusSm
radius: 12
border.width: variant === "outlined" ? 1 : 0
border.color: _accentColor
// MD3 left accent bar
Rectangle {
id: accentBar
width: 4
height: parent.height - 8
anchors.left: parent.left
anchors.leftMargin: 4
anchors.verticalCenter: parent.verticalCenter
radius: 2
color: root._severityColor
visible: root.variant !== "outlined"
}
// Content
RowLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: StyleVariables.spacingMd
spacing: StyleVariables.spacingSm
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.topMargin: 16
anchors.bottomMargin: 16
spacing: 12
// Icon
Text {
text: root._effectiveIcon
font.pixelSize: StyleVariables.fontSizeLg
font.pixelSize: 20
Layout.alignment: Qt.AlignTop
}
// Text content
ColumnLayout {
Layout.fillWidth: true
spacing: StyleVariables.spacingXs
spacing: 4
// Title (optional)
Text {
visible: root.title
visible: root.title !== ""
text: root.title
font.pixelSize: StyleVariables.fontSizeSm
font.weight: Font.Bold
color: root._textColor
font.pixelSize: 13
font.weight: Font.DemiBold
color: root._severityColor
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
// Message
Text {
text: root.text
font.pixelSize: StyleVariables.fontSizeSm
color: root._textColor
font.pixelSize: 14
color: root.title ? Theme.text : root._textColor
opacity: root.title ? 0.9 : 1.0
Layout.fillWidth: true
wrapMode: Text.WordWrap
lineHeight: 1.4
}
}
// Close button
Text {
Rectangle {
visible: root.closable
text: "✕"
font.pixelSize: StyleVariables.fontSizeSm
color: Theme.textSecondary
width: 28
height: 28
radius: 14
color: closeArea.containsMouse ? Qt.rgba(root._severityColor.r, root._severityColor.g, root._severityColor.b, 0.12) : "transparent"
Layout.alignment: Qt.AlignTop
Text {
anchors.centerIn: parent
text: "✕"
font.pixelSize: 14
color: Theme.textSecondary
}
MouseArea {
id: closeArea
anchors.fill: parent
anchors.margins: -8
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: root.closed()
}
}
}
// Entry animation
opacity: 0
Component.onCompleted: opacity = 1
Behavior on opacity { NumberAnimation { duration: StyleVariables.transitionNormal } }
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
}

View File

@@ -4,124 +4,154 @@ import QtQuick.Layouts
import QmlComponents 1.0
/**
* CDialog.qml - Modal dialog (mirrors _dialog.scss)
* CDialog.qml - Material Design 3 Dialog
* Overlay dialog with header, content, and footer
*/
Popup {
id: root
property string title: ""
property string size: "md" // sm, md, lg, xl
property bool showClose: true
property alias dialogContent: contentArea.data
property alias footerItem: footerArea.data
// Size mapping
// MD3 size mapping (min 280, max 560 for default)
readonly property int _maxWidth: {
switch (size) {
case "sm": return 400
case "lg": return 800
case "xl": return 1000
default: return 640
case "sm": return 360
case "lg": return 560
case "xl": return 560
default: return 480
}
}
readonly property int _minWidth: 280
modal: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
anchors.centerIn: parent
width: Math.min(_maxWidth, parent.width - StyleVariables.spacingLg * 2)
// Fade in animation
width: Math.max(_minWidth, Math.min(_maxWidth, parent.width - 48))
// MD3 open animation: scale + opacity
enter: Transition {
ParallelAnimation {
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 150 }
NumberAnimation { property: "y"; from: root.y - 20; to: root.y; duration: 200; easing.type: Easing.OutCubic }
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 250; easing.type: Easing.OutCubic }
NumberAnimation { property: "scale"; from: 0.85; to: 1.0; duration: 250; easing.type: Easing.OutCubic }
}
}
// MD3 close animation
exit: Transition {
NumberAnimation { property: "opacity"; from: 1; to: 0; duration: 100 }
ParallelAnimation {
NumberAnimation { property: "opacity"; from: 1; to: 0; duration: 200; easing.type: Easing.InCubic }
NumberAnimation { property: "scale"; from: 1.0; to: 0.85; duration: 200; easing.type: Easing.InCubic }
}
}
// Overlay background
// MD3 scrim/backdrop
Overlay.modal: Rectangle {
color: Qt.rgba(0, 0, 0, 0.6)
Behavior on opacity { NumberAnimation { duration: 150 } }
color: Qt.rgba(0, 0, 0, 0.4)
Behavior on opacity { NumberAnimation { duration: 200 } }
}
// MD3 surface container background with large radius
background: Rectangle {
color: Theme.surface
radius: StyleVariables.radiusLg
radius: 28
// MD3 elevation shadow
layer.enabled: true
layer.effect: Item {
Rectangle {
anchors.fill: parent
anchors.margins: -10
anchors.margins: -8
color: "transparent"
Rectangle {
anchors.fill: parent
anchors.margins: 10
anchors.margins: 8
color: "#000000"
opacity: 0.3
radius: StyleVariables.radiusLg + 4
opacity: 0.18
radius: 32
}
}
}
}
contentItem: ColumnLayout {
spacing: 0
// Header
RowLayout {
ColumnLayout {
Layout.fillWidth: true
Layout.margins: StyleVariables.spacingMd
Layout.bottomMargin: 0
spacing: StyleVariables.spacingMd
Layout.topMargin: 24
Layout.leftMargin: 24
Layout.rightMargin: 24
Layout.bottomMargin: 16
spacing: 0
visible: root.title !== "" || root.showClose
Text {
RowLayout {
Layout.fillWidth: true
text: root.title
color: Theme.onSurface
font.pixelSize: StyleVariables.fontSizeLg
font.weight: Font.DemiBold
elide: Text.ElideRight
}
CIconButton {
visible: root.showClose
icon: "✕"
size: "sm"
onClicked: root.close()
spacing: 8
Text {
Layout.fillWidth: true
text: root.title
color: Theme.text
font.pixelSize: 24
font.weight: Font.Bold
elide: Text.ElideRight
}
// MD3 close button (circular, subtle)
Rectangle {
visible: root.showClose
width: 32
height: 32
radius: 16
color: closeHover.containsMouse ? Theme.actionHover : "transparent"
Text {
anchors.centerIn: parent
text: "✕"
font.pixelSize: 16
color: Theme.textSecondary
}
MouseArea {
id: closeHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.close()
}
}
}
}
// Divider after header
CDivider {
Layout.fillWidth: true
visible: root.title !== ""
}
// Content area
Item {
id: contentArea
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: StyleVariables.spacingLg
Layout.leftMargin: 24
Layout.rightMargin: 24
Layout.bottomMargin: 8
implicitHeight: childrenRect.height
}
// Footer (optional)
// Footer with right-aligned text buttons (MD3 pattern)
Item {
id: footerArea
Layout.fillWidth: true
Layout.margins: StyleVariables.spacingMd
Layout.topMargin: 0
Layout.leftMargin: 24
Layout.rightMargin: 24
Layout.bottomMargin: 24
Layout.topMargin: 8
implicitHeight: childrenRect.height
visible: children.length > 0
}

View File

@@ -4,49 +4,49 @@ import QtQuick.Effects
import QmlComponents 1.0
/**
* CSnackbar.qml - Snackbar/toast notification (mirrors _snackbar.scss)
* CSnackbar.qml - Material Design 3 Snackbar/toast notification
* Brief messages at the bottom of the screen
*
*
* Usage:
* CSnackbar {
* id: snackbar
* }
*
*
* // Show snackbar
* snackbar.show("Message saved!", "success")
* snackbar.show("Error occurred", "error", 5000, "Retry", () => { retry() })
*/
Item {
id: root
// Public properties
property int duration: 4000 // Auto-hide duration in ms (0 = no auto-hide)
property string position: "bottom" // bottom, top
property int maxWidth: 400
property int maxWidth: 480
// Internal state
property string _message: ""
property string _severity: "default" // default, success, warning, error, info
property string _actionText: ""
property var _actionCallback: null
property bool _visible: false
// Signals
signal actionClicked()
// Size
width: parent.width
height: snackbarRect.height + StyleVariables.spacingLg * 2
height: snackbarRect.height + 24 * 2
// Position
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: position === "bottom" ? parent.bottom : undefined
anchors.top: position === "top" ? parent.top : undefined
// Z-index
z: StyleVariables.zToast
// Show snackbar
function show(message, severity, customDuration, actionText, actionCallback) {
_message = message || ""
@@ -54,93 +54,125 @@ Item {
_actionText = actionText || ""
_actionCallback = actionCallback || null
_visible = true
// Start auto-hide timer
if ((customDuration !== undefined ? customDuration : duration) > 0) {
hideTimer.interval = customDuration !== undefined ? customDuration : duration
hideTimer.restart()
}
}
// Hide snackbar
function hide() {
_visible = false
hideTimer.stop()
}
// Auto-hide timer
Timer {
id: hideTimer
onTriggered: root.hide()
}
// MD3 inverse surface color
readonly property color _inverseSurface: Theme.mode === "dark" ? "#E6E1E5" : "#313033"
// MD3 inverse on surface (text color)
readonly property color _inverseOnSurface: Theme.mode === "dark" ? "#313033" : "#F4EFF4"
// MD3 inverse primary (action button color)
readonly property color _inversePrimary: Theme.mode === "dark" ? Qt.darker(Theme.primary, 1.3) : Qt.lighter(Theme.primary, 1.4)
// Snackbar content
Rectangle {
id: snackbarRect
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: root.position === "bottom" ? parent.bottom : undefined
anchors.top: root.position === "top" ? parent.top : undefined
anchors.margins: StyleVariables.spacingMd
width: Math.min(contentRow.implicitWidth + StyleVariables.spacingMd * 2, root.maxWidth)
height: contentRow.implicitHeight + StyleVariables.spacingSm * 2
radius: StyleVariables.radiusSm
color: {
switch (root._severity) {
case "success": return "#1b5e20"
case "warning": return "#e65100"
case "error": return "#b71c1c"
case "info": return "#0d47a1"
default: return "#323232"
}
}
// Shadow
anchors.bottomMargin: 16
anchors.topMargin: 16
width: Math.min(contentRow.implicitWidth + 16 * 2, root.maxWidth)
height: contentRow.implicitHeight + 14 * 2
radius: 8
color: root._inverseSurface
// MD3 elevation 3 shadow
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowColor: "#60000000"
shadowBlur: 0.3
shadowVerticalOffset: 4
shadowColor: "#40000000"
shadowBlur: 0.4
shadowVerticalOffset: 6
shadowHorizontalOffset: 0
}
// Visibility animation
// Slide-in animation from bottom/top
opacity: root._visible ? 1 : 0
y: root._visible ? 0 : (root.position === "bottom" ? 20 : -20)
Behavior on opacity { NumberAnimation { duration: StyleVariables.transitionNormal } }
Behavior on y { NumberAnimation { duration: StyleVariables.transitionNormal; easing.type: Easing.OutCubic } }
transform: Translate {
y: root._visible ? 0 : (root.position === "bottom" ? 60 : -60)
Behavior on y {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
}
Behavior on opacity {
NumberAnimation {
duration: root._visible ? 250 : 200
easing.type: root._visible ? Easing.OutCubic : Easing.InCubic
}
}
// Content
RowLayout {
id: contentRow
anchors.centerIn: parent
spacing: StyleVariables.spacingMd
// Message
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 16
anchors.rightMargin: 16
spacing: 12
// Message text — MD3 inverse on surface
Text {
text: root._message
font.pixelSize: StyleVariables.fontSizeSm
color: "#ffffff"
Layout.maximumWidth: root.maxWidth - StyleVariables.spacingMd * 4 - (actionButton.visible ? actionButton.width : 0)
font.pixelSize: 14
font.letterSpacing: 0.25
color: root._inverseOnSurface
Layout.fillWidth: true
Layout.maximumWidth: root.maxWidth - 16 * 3 - (actionButton.visible ? actionButton.width + 12 : 0)
wrapMode: Text.WordWrap
lineHeight: 1.4
}
// Action button
Text {
// Action button — MD3 inverse primary, text style
Rectangle {
id: actionButton
visible: root._actionText
text: root._actionText
font.pixelSize: StyleVariables.fontSizeSm
font.weight: Font.Bold
color: Theme.primaryLight
visible: root._actionText !== ""
width: actionLabel.implicitWidth + 16
height: actionLabel.implicitHeight + 12
radius: 4
color: actionArea.containsMouse ? Qt.rgba(root._inversePrimary.r, root._inversePrimary.g, root._inversePrimary.b, 0.12) : "transparent"
Text {
id: actionLabel
anchors.centerIn: parent
text: root._actionText
font.pixelSize: 14
font.weight: Font.DemiBold
font.letterSpacing: 0.1
color: root._inversePrimary
}
MouseArea {
id: actionArea
anchors.fill: parent
anchors.margins: -StyleVariables.spacingXs
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
if (root._actionCallback) {
root._actionCallback()
@@ -151,8 +183,8 @@ Item {
}
}
}
// Close on click (optional)
// Dismiss on click
MouseArea {
anchors.fill: parent
z: -1

View File

@@ -4,49 +4,205 @@ import QtQuick.Layouts
import QmlComponents 1.0
/**
* CAutocomplete.qml - simple autocomplete input with popup suggestions
* CAutocomplete.qml - Material Design 3 autocomplete input with popup suggestions
* TextField + dropdown popup with 8px radius, surface background, and elevation
*/
Item {
id: root
width: 300
implicitHeight: input.height
property alias text: input.text
property var suggestions: []
property Component delegate: null
property string placeholderText: "Type to search..."
property bool loading: false
signal accepted(string value)
TextField {
id: input
anchors.left: parent.left
anchors.right: parent.right
placeholderText: "Type to search..."
placeholderText: root.placeholderText
color: Theme.text
font.pixelSize: 14
font.family: Theme.fontFamily
placeholderTextColor: Theme.textSecondary
background: Rectangle {
radius: 8
color: Theme.surface
border.width: input.activeFocus ? 2 : 1
border.color: input.activeFocus ? Theme.primary : Theme.border
Behavior on border.color {
ColorAnimation { duration: Theme.transitionShortest }
}
Behavior on border.width {
NumberAnimation { duration: Theme.transitionShortest }
}
}
onTextChanged: {
popup.open()
if (text.length > 0 && root.suggestions.length > 0) {
popup.open()
} else {
popup.close()
}
}
onAccepted: root.accepted(text)
}
Popup {
id: popup
x: input.mapToItem(root, 0, input.height).x
y: input.mapToItem(root, 0, input.height).y
y: input.height + 4
width: input.width
modal: false
focus: true
focus: false
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
padding: 0
background: Rectangle {
color: Theme.surface
radius: 8
border.color: Theme.border
border.width: 1
// MD3 elevation shadow
layer.enabled: true
layer.effect: Item {
Rectangle {
anchors.fill: parent
anchors.margins: -2
radius: 10
color: "transparent"
border.color: "transparent"
Rectangle {
anchors.fill: parent
anchors.topMargin: 2
radius: 10
color: Theme.shadowColor
opacity: 0.15
}
}
}
}
contentItem: ListView {
id: list
implicitHeight: Math.min(240, contentHeight)
model: root.suggestions
clip: true
interactive: true
boundsBehavior: Flickable.StopAtBounds
delegate: root.delegate ? root.delegate : defaultDelegate
}
}
Component {
id: defaultDelegate
Rectangle {
width: parent.width
color: Theme.surface
radius: StyleVariables.radiusSm
border.color: Theme.divider
width: list.width
height: 48
color: delegateMouseArea.containsMouse ? Theme.actionHover : "transparent"
radius: 0
ListView {
id: list
width: parent.width
model: root.suggestions
delegate: root.delegate ? root.delegate : ItemDelegate { text: modelData; onClicked: { root.input.text = modelData; popup.close(); root.accepted(modelData); } }
clip: true
interactive: true
height: Math.min(200, contentHeight)
Text {
id: delegateText
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 16
anchors.rightMargin: 16
text: modelData
color: Theme.text
font.pixelSize: 14
font.family: Theme.fontFamily
elide: Text.ElideRight
}
// Highlight matching text overlay
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 16
anchors.rightMargin: 16
font.pixelSize: 14
font.family: Theme.fontFamily
elide: Text.ElideRight
color: "transparent"
visible: input.text.length > 0
// Build rich text with highlighted match
textFormat: Text.RichText
text: {
var src = modelData
var query = input.text.toLowerCase()
var idx = src.toLowerCase().indexOf(query)
if (idx < 0) return ""
var before = src.substring(0, idx)
var match = src.substring(idx, idx + query.length)
var after = src.substring(idx + query.length)
return before + "<font color='" + Theme.primary + "'>" + match + "</font>" + after
}
}
// Second pass: draw the full text with highlighted portion
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 16
anchors.rightMargin: 16
font.pixelSize: 14
font.family: Theme.fontFamily
elide: Text.ElideRight
textFormat: Text.RichText
visible: input.text.length > 0
text: {
var src = modelData
var query = input.text.toLowerCase()
var idx = src.toLowerCase().indexOf(query)
if (idx < 0) return "<font color='" + Theme.text + "'>" + src + "</font>"
var before = src.substring(0, idx)
var match = src.substring(idx, idx + query.length)
var after = src.substring(idx + query.length)
return "<font color='" + Theme.text + "'>" + before + "</font>"
+ "<font color='" + Theme.primary + "'><b>" + match + "</b></font>"
+ "<font color='" + Theme.text + "'>" + after + "</font>"
}
}
// Hide plain text when highlighted version is showing
Component.onCompleted: {
delegateText.visible = Qt.binding(function() { return input.text.length === 0 })
}
MouseArea {
id: delegateMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.text = modelData
popup.close()
root.accepted(modelData)
}
}
// State layer ripple on press
Rectangle {
anchors.fill: parent
color: Theme.primary
opacity: delegateMouseArea.pressed ? 0.12 : 0
Behavior on opacity {
NumberAnimation { duration: Theme.transitionShortest }
}
}
}
}

View File

@@ -3,58 +3,78 @@ import QtQuick.Layouts
import QmlComponents 1.0
/**
* CFormGroup.qml - Form field container (mirrors _form.scss)
* Groups label, input, and helper/error text
* CFormGroup.qml - Material Design 3 form field container
* Groups label, input, and helper/error text with MD3 spacing
*/
ColumnLayout {
id: root
property string label: ""
property string helperText: ""
property string errorText: ""
property bool required: false
property bool disabled: false
property bool focused: false
default property alias content: contentArea.data
spacing: StyleVariables.spacingXs
spacing: 0
// Label
RowLayout {
Layout.fillWidth: true
spacing: 2
Layout.bottomMargin: 4
spacing: 0
visible: root.label !== ""
Text {
text: root.label
color: root.disabled ? Theme.onSurfaceVariant : Theme.onSurface
font.pixelSize: StyleVariables.fontSizeSm
color: {
if (root.disabled) return Theme.textDisabled
if (root.errorText) return Theme.error
if (root.focused) return Theme.primary
return Theme.textSecondary
}
font.pixelSize: 12
font.weight: Font.Medium
opacity: root.disabled ? 0.6 : 1
font.family: Theme.fontFamily
Behavior on color {
ColorAnimation { duration: Theme.transitionShortest }
}
}
Text {
text: "*"
text: " *"
color: Theme.error
font.pixelSize: StyleVariables.fontSizeSm
font.pixelSize: 12
font.weight: Font.Medium
font.family: Theme.fontFamily
visible: root.required
}
}
// Content slot (for input)
Item {
id: contentArea
Layout.fillWidth: true
implicitHeight: childrenRect.height
}
// Helper or error text
Text {
Layout.fillWidth: true
Layout.topMargin: 4
text: root.errorText || root.helperText
color: root.errorText ? Theme.error : Theme.onSurfaceVariant
font.pixelSize: StyleVariables.fontSizeXs
color: root.errorText ? Theme.error : Theme.textSecondary
font.pixelSize: 12
font.family: Theme.fontFamily
visible: text !== ""
wrapMode: Text.Wrap
opacity: root.disabled ? 0.38 : 1
Behavior on color {
ColorAnimation { duration: Theme.transitionShortest }
}
}
}

View File

@@ -2,11 +2,46 @@ import QtQuick
import QtQuick.Controls
import QmlComponents 1.0
/**
* CFormLabel.qml - Material Design 3 form label
* 12px medium-weight label with focus, error, and required states
*/
Text {
id: label
property alias text: label.text
property bool required: false
color: Theme.onSurface
font.pixelSize: StyleVariables.fontSizeSm
property bool focused: false
property bool error: false
property bool disabled: false
text: "Label"
color: {
if (disabled) return Theme.textDisabled
if (error) return Theme.error
if (focused) return Theme.primary
return Theme.textSecondary
}
font.pixelSize: 12
font.weight: Font.Medium
font.family: Theme.fontFamily
opacity: disabled ? 0.38 : 1
// Append required asterisk via overlay to keep text property clean
Text {
anchors.left: parent.right
anchors.baseline: parent.baseline
text: " *"
color: Theme.error
font.pixelSize: 12
font.weight: Font.Medium
font.family: Theme.fontFamily
visible: label.required
}
Behavior on color {
ColorAnimation { duration: Theme.transitionShortest }
}
}

View File

@@ -2,28 +2,41 @@ import QtQuick
import QmlComponents 1.0
/**
* CLabel.qml - Form label (mirrors _label.scss)
* Styled label for form inputs
* CLabel.qml - Material Design 3 simple text label
* 14px body label associated with a form control
*/
Text {
id: root
property string size: "md" // sm, md, lg
property bool required: false
property bool disabled: false
color: disabled ? Theme.onSurfaceVariant : Theme.onSurface
opacity: disabled ? 0.6 : 1
property Item control: null
color: disabled ? Theme.textDisabled : Theme.text
opacity: disabled ? 0.38 : 1
font.pixelSize: {
switch (size) {
case "sm": return StyleVariables.fontSizeXs
case "lg": return StyleVariables.fontSizeMd
default: return StyleVariables.fontSizeSm
case "sm": return 12
case "lg": return 16
default: return 14
}
}
font.weight: Font.Medium
font.weight: Font.Normal
font.family: Theme.fontFamily
// Append asterisk for required fields
text: text + (required ? " *" : "")
// Clicking the label focuses the associated control
MouseArea {
anchors.fill: parent
cursorShape: root.control ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (root.control && root.control.forceActiveFocus) {
root.control.forceActiveFocus()
}
}
}
}

View File

@@ -1,15 +1,60 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "../theming"
Row {
id: root
spacing: 8
property var items: []
property var items: [] // ["Home", "Settings", "Profile"]
property string separator: "/" // "/" or ">"
signal itemClicked(int index, string label)
spacing: 0
Repeater {
model: root.items
delegate: Row {
Text { text: modelData }
Text { text: index < root.items.length-1 ? ">" : ""; color: "#888" }
Row {
spacing: 0
// Breadcrumb item — last item is plain text, others are clickable
Text {
id: crumbText
text: modelData
font.pixelSize: 14
font.weight: Font.Medium
color: {
if (index === root.items.length - 1)
return Theme.text
if (crumbMouse.containsMouse)
return Theme.primary
return Theme.textSecondary
}
opacity: index === root.items.length - 1 ? 1.0 : (crumbMouse.containsMouse ? 1.0 : 0.9)
verticalAlignment: Text.AlignVCenter
Behavior on color { ColorAnimation { duration: 150 } }
MouseArea {
id: crumbMouse
anchors.fill: parent
hoverEnabled: index < root.items.length - 1
cursorShape: index < root.items.length - 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: index < root.items.length - 1
onClicked: root.itemClicked(index, modelData)
}
}
// Separator — not shown after last item
Text {
text: " " + root.separator + " "
font.pixelSize: 14
color: Theme.textSecondary
visible: index < root.items.length - 1
verticalAlignment: Text.AlignVCenter
}
}
}
}

View File

@@ -1,87 +1,109 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "../theming"
Rectangle {
id: tabBar
property int currentIndex: 0
property var tabs: [] // [{label: "Tab 1", icon: "🏠"}]
property var tabs: [] // [{label: "Tab 1", icon: "home"}]
signal tabClicked(int index)
implicitHeight: 48
color: "transparent"
RowLayout {
anchors.fill: parent
spacing: 0
Repeater {
id: tabRepeater
model: tabBar.tabs
Rectangle {
id: tabDelegate
readonly property bool isActive: tabBar.currentIndex === index
Layout.fillHeight: true
Layout.preferredWidth: tabText.implicitWidth + 32
color: tabBar.currentIndex === index ? "#2d2d2d" : (tabMouse.containsMouse ? "#252525" : "transparent")
Behavior on color { ColorAnimation { duration: 150 } }
ColumnLayout {
Layout.preferredWidth: tabContent.implicitWidth + 32
color: "transparent"
// MD3 state layer on hover
Rectangle {
anchors.fill: parent
color: Theme.text
opacity: tabMouse.containsMouse && !tabDelegate.isActive ? 0.08 : 0
Behavior on opacity { NumberAnimation { duration: 150 } }
}
RowLayout {
id: tabContent
anchors.centerIn: parent
spacing: 4
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 6
Text {
text: modelData.icon || ""
font.pixelSize: 14
visible: modelData.icon
}
Text {
id: tabText
text: modelData.label || modelData
font.pixelSize: 13
font.weight: tabBar.currentIndex === index ? Font.DemiBold : Font.Normal
color: tabBar.currentIndex === index ? "#4dabf7" : "#888888"
Behavior on color { ColorAnimation { duration: 150 } }
}
spacing: 6
Text {
text: modelData.icon || ""
font.pixelSize: 14
color: tabDelegate.isActive ? Theme.primary : Theme.textSecondary
visible: modelData.icon !== undefined && modelData.icon !== ""
Behavior on color { ColorAnimation { duration: 150 } }
}
Text {
text: modelData.label || modelData
font.pixelSize: 14
font.weight: tabDelegate.isActive ? Font.DemiBold : Font.Medium
color: tabDelegate.isActive ? Theme.primary : Theme.textSecondary
Behavior on color { ColorAnimation { duration: 150 } }
}
}
// Active indicator
Rectangle {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 16
height: 3
radius: 1.5
color: "#4dabf7"
visible: tabBar.currentIndex === index
}
MouseArea {
id: tabMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: tabBar.currentIndex = index
onClicked: {
tabBar.currentIndex = index
tabBar.tabClicked(index)
}
}
}
}
Item { Layout.fillWidth: true }
}
// Bottom border
// MD3 active indicator pill — animates x and width smoothly
Rectangle {
id: activeIndicator
anchors.bottom: parent.bottom
height: 3
radius: 1.5
color: Theme.primary
visible: tabRepeater.count > 0
property Item targetTab: tabRepeater.count > 0 && tabBar.currentIndex >= 0 && tabBar.currentIndex < tabRepeater.count
? tabRepeater.itemAt(tabBar.currentIndex) : null
x: targetTab ? targetTab.x + 8 : 0
width: targetTab ? targetTab.width - 16 : 0
Behavior on x { NumberAnimation { duration: 250; easing.type: Easing.OutCubic } }
Behavior on width { NumberAnimation { duration: 250; easing.type: Easing.OutCubic } }
}
// Subtle bottom divider
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: 1
color: "#2d2d2d"
color: Theme.border
opacity: 0.4
}
}