From de3a3ac19436b75514ad77fb3e97e4a0bc8b8d2b Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Thu, 19 Mar 2026 04:03:58 +0000 Subject: [PATCH] =?UTF-8?q?feat(qml):=20MD3=20rework=20batch=202=20?= =?UTF-8?q?=E2=80=94=2017=20more=20components=20rewritten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- qml/components/atoms/CCodeBlock.qml | 83 ++++----- qml/components/atoms/CCodeInline.qml | 26 +-- qml/components/atoms/CText.qml | 108 +++++++++--- qml/components/atoms/CTitle.qml | 57 ++---- qml/components/core/CListItem.qml | 102 ++++++----- qml/components/data-display/CAvatar.qml | 75 ++++++-- qml/components/data-display/CDivider.qml | 43 +++-- qml/components/data-display/CTable.qml | 192 +++++++++++++++------ qml/components/feedback/CAlert.qml | 121 ++++++++----- qml/components/feedback/CDialog.qml | 154 ++++++++++------- qml/components/feedback/CSnackbar.qml | 158 ++++++++++------- qml/components/form/CAutocomplete.qml | 192 +++++++++++++++++++-- qml/components/form/CFormGroup.qml | 58 +++++-- qml/components/form/CFormLabel.qml | 39 ++++- qml/components/form/CLabel.qml | 37 ++-- qml/components/navigation/CBreadcrumbs.qml | 55 +++++- qml/components/navigation/CTabBar.qml | 124 +++++++------ 17 files changed, 1092 insertions(+), 532 deletions(-) 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 } }