diff --git a/qml/components/atoms/CCodeBlock.qml b/qml/components/atoms/CCodeBlock.qml
index 7d46f4c82..8ef88946c 100644
--- a/qml/components/atoms/CCodeBlock.qml
+++ b/qml/components/atoms/CCodeBlock.qml
@@ -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
}
diff --git a/qml/components/atoms/CCodeInline.qml b/qml/components/atoms/CCodeInline.qml
index bde947e67..bccbf1d16 100644
--- a/qml/components/atoms/CCodeInline.qml
+++ b/qml/components/atoms/CCodeInline.qml
@@ -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
}
}
diff --git a/qml/components/atoms/CText.qml b/qml/components/atoms/CText.qml
index 1560a9655..321ee89d2 100644
--- a/qml/components/atoms/CText.qml
+++ b/qml/components/atoms/CText.qml
@@ -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
diff --git a/qml/components/atoms/CTitle.qml b/qml/components/atoms/CTitle.qml
index df2c69c7d..9a723c149 100644
--- a/qml/components/atoms/CTitle.qml
+++ b/qml/components/atoms/CTitle.qml
@@ -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
}
diff --git a/qml/components/core/CListItem.qml b/qml/components/core/CListItem.qml
index ff1531031..aa9b98221 100644
--- a/qml/components/core/CListItem.qml
+++ b/qml/components/core/CListItem.qml
@@ -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
}
}
diff --git a/qml/components/data-display/CAvatar.qml b/qml/components/data-display/CAvatar.qml
index b2a21b741..23b84315e 100644
--- a/qml/components/data-display/CAvatar.qml
+++ b/qml/components/data-display/CAvatar.qml
@@ -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 !== ""
}
}
diff --git a/qml/components/data-display/CDivider.qml b/qml/components/data-display/CDivider.qml
index f30de4d0e..600de2bd6 100644
--- a/qml/components/data-display/CDivider.qml
+++ b/qml/components/data-display/CDivider.qml
@@ -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
}
}
diff --git a/qml/components/data-display/CTable.qml b/qml/components/data-display/CTable.qml
index c8e1a571f..569dc417f 100644
--- a/qml/components/data-display/CTable.qml
+++ b/qml/components/data-display/CTable.qml
@@ -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
+ }
}
}
}
diff --git a/qml/components/feedback/CAlert.qml b/qml/components/feedback/CAlert.qml
index a1ac2c2ad..c3ad7a76b 100644
--- a/qml/components/feedback/CAlert.qml
+++ b/qml/components/feedback/CAlert.qml
@@ -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 } }
}
diff --git a/qml/components/feedback/CDialog.qml b/qml/components/feedback/CDialog.qml
index 6f4a9def9..c4ed67afe 100644
--- a/qml/components/feedback/CDialog.qml
+++ b/qml/components/feedback/CDialog.qml
@@ -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
}
diff --git a/qml/components/feedback/CSnackbar.qml b/qml/components/feedback/CSnackbar.qml
index 0f10b4634..299b60b96 100644
--- a/qml/components/feedback/CSnackbar.qml
+++ b/qml/components/feedback/CSnackbar.qml
@@ -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
diff --git a/qml/components/form/CAutocomplete.qml b/qml/components/form/CAutocomplete.qml
index cf1617e59..f204ce474 100644
--- a/qml/components/form/CAutocomplete.qml
+++ b/qml/components/form/CAutocomplete.qml
@@ -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 + "" + match + "" + 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 "" + src + ""
+ var before = src.substring(0, idx)
+ var match = src.substring(idx, idx + query.length)
+ var after = src.substring(idx + query.length)
+ return "" + before + ""
+ + "" + match + ""
+ + "" + after + ""
+ }
+ }
+
+ // 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 }
+ }
}
}
}
diff --git a/qml/components/form/CFormGroup.qml b/qml/components/form/CFormGroup.qml
index 182502172..9305516a6 100644
--- a/qml/components/form/CFormGroup.qml
+++ b/qml/components/form/CFormGroup.qml
@@ -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 }
+ }
}
}
diff --git a/qml/components/form/CFormLabel.qml b/qml/components/form/CFormLabel.qml
index 31d30d908..d6431c49d 100644
--- a/qml/components/form/CFormLabel.qml
+++ b/qml/components/form/CFormLabel.qml
@@ -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 }
+ }
}
diff --git a/qml/components/form/CLabel.qml b/qml/components/form/CLabel.qml
index a95b56efe..fd3aeb503 100644
--- a/qml/components/form/CLabel.qml
+++ b/qml/components/form/CLabel.qml
@@ -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()
+ }
+ }
+ }
}
diff --git a/qml/components/navigation/CBreadcrumbs.qml b/qml/components/navigation/CBreadcrumbs.qml
index 0cfccf8be..0ff4319dd 100644
--- a/qml/components/navigation/CBreadcrumbs.qml
+++ b/qml/components/navigation/CBreadcrumbs.qml
@@ -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
+ }
}
}
}
diff --git a/qml/components/navigation/CTabBar.qml b/qml/components/navigation/CTabBar.qml
index 3914680db..54d615c02 100644
--- a/qml/components/navigation/CTabBar.qml
+++ b/qml/components/navigation/CTabBar.qml
@@ -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
}
}